Blog

How I generate the JavaFlight Recorder Docu

From a tweet von Gunnar Morling View tweet I learned today that my work on JFR-Doc is used for JfrUnit. In a follow up Gunnar asked me how those JSON-Files are generated and I promised to write up a blog what I hacked together in an evening to get this going.

Get the input

The first and most important thing is to get all JFR-Events (those commands are executed eg in your Java-17-Install-Dir)

./java -XX:StartFlightRecording:filename=/tmp/out-bin.jfr \
-version # Dump JFR Data
./jfr metadata /tmp/out-bin.jfr > /tmp/openjdk-17.jfr

The final file holds content like this

class boolean {
}
class byte {
}
class char {
}
class double {
}
class float {
}
class int {
}
class long {
}
class short {
}

@Name("java.lang.Class")
@Label("Java Class")
class Class {
  @Label("Class Loader")
  ClassLoader classLoader;

  @Label("Name")
  String name;

  @Label("Package")
  Package package;

  @Label("Access Modifiers")
  int modifiers;

  @Label("Hidden")
  boolean hidden;
}
...

Looks like these are Java-Classes so one strategy could be to just compile those and use Reflection to extract meta informations but I went another route

Parsing the .jfr-File

Handcrafting a parser is certainly not the way to go. I needed something that could provide me a fairly simple Logical-AST. There are BNF-Definitions for Java but I wanted something much simpler so I fired up my Eclipse IDE and created an Xtext-Project using the wizards and replaced the content in the .xtext-File with

grammar at.bestsolution.jfr.JFRMeta with org.eclipse.xtext.common.Terminals

generate jFRMeta "http://www.bestsolution.at/jfr/JFRMeta"

Model:
  classes+=Clazz*;

Clazz:
  annotations+=Annotation*
  'class' name=ID ( 'extends' super=QualifiedName )? '{'
    attributes += Attribute*
  '}';

Attribute:
  annotations+=Annotation*
  type=[Clazz|ID] array?='[]'? name=ID ';'
;

Annotation:
  '@' type=[Clazz|ID] ('(' (values+=AnnotationValue |
   ('{' values+=AnnotationValue
   (',' values += AnnotationValue)* '}')) ')')?
;

AnnotationValue:
  valueString=STRING | valueBoolean=Boolean | valueNum=INT
;

enum Boolean:
  TRUE="true" | FALSE="false"
;

QualifiedName:
  ID ('.' ID)*;

That's all required because the .jfr-File is extremly simple so we don't need a more complex definition.

How to convert

Well although Xtext is primarily used to develop DSL-Editors for the Eclipse IDE one can run the generated parser in plain old Java. So all now needed is to write a generator who parses the .jfr-File(s) and generate different output from it (HTML, JSON, ...) and because although Java now has multiline strings Xtend is the much better choice to write a "code"-generator.

package at.bestsolution.jfr

import org.eclipse.xtext.resource.XtextResourceSet
import org.eclipse.xtext.resource.XtextResource
import java.util.ArrayList
import org.eclipse.emf.common.util.URI
import java.nio.file.Files
import java.nio.file.Paths
import at.bestsolution.jfr.jFRMeta.Model
import java.nio.file.StandardOpenOption
import at.bestsolution.jfr.jFRMeta.Clazz

import static extension at.bestsolution.jfr.GenUtil.*
import at.bestsolution.jfr.jFRMeta.Attribute

class JSONGen {
     def static void main(String[] args) {
         val versions = createVersionList(Integer.parseInt(args.get(0)))
         val injector = new JFRMetaStandaloneSetup().createInjectorAndDoEMFRegistration();
         val resourceSet = injector.getInstance(XtextResourceSet);
         resourceSet.addLoadOption(XtextResource.OPTION_RESOLVE_ALL, Boolean.TRUE);

         val models = new ArrayList
         for( v : versions ) {
             val resource = resourceSet.getResource(
                 URI.createURI("file:/Users/tomschindl/git/jfr-doc/openjdk-"+v+".jfr"), true);
             val model = resource.getContents().head as Model;
             models.add(model)
         }

         for( pair : models.indexed ) {
             val model = pair.value
             var version = versions.get(pair.key)
             val preModel = pair.key == 0 ? null : models.get(pair.key - 1)
             Files.writeString(Paths.get("/Users/tomschindl/git/jfr-doc/openjdk-"+version+".json"),model.generate(preModel,version), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE)
         }
     }

     def static generate(Model model, Model prevModel, String ver) '''
         {
             "version": "«ver»",
             "distribution": "openjdk",
             "events": [
                 «val evts = model.classes.filter[c|c.super == "jdk.jfr.Event"]»
                 «FOR e : evts»
                     «e.generateEvent»«IF e !== evts.last»,«ENDIF»
                 «ENDFOR»
             ],
             "types": [
                 «val types = model.classes.filter[c|c.super === null]»
                 «FOR t : types»
                     «t.generateType»«IF t !== types.last»,«ENDIF»
                 «ENDFOR»
             ]
         }
     '''

     def static generateEvent(Clazz clazz) '''
         {
             "name": "«clazz.name»",
             "description": "«clazz.description»",
             "label": "«clazz.label»",
             "categories": [
                 «val cats = clazz.categories»
                 «FOR cat : cats»
                     "«cat»"«IF cat !== cats.last»,«ENDIF»
                 «ENDFOR»
             ],
             "attributes": [
                 «FOR a : clazz.attributes»
                     «a.generateAttribute»«IF a !== clazz.attributes.last»,«ENDIF»
                 «ENDFOR»
             ]
         }
     '''

     def static generateType(Clazz clazz) '''
         {
             "name": "«clazz.name»",
             "attributes": [
                 «FOR a : clazz.attributes»
                     «a.generateAttribute»«IF a !== clazz.attributes.last»,«ENDIF»
                 «ENDFOR»
             ]
         }
     '''

     def static generateAttribute(Attribute a) '''
         {
             "name": "«a.name»",
             "type": "«a.type.name»",
             "contentType": "«a.contentType»",
             "description": "«a.description»"
         }
     '''
}

All sources are available at github.com if you look at this code keep in mind that it was hacked together in an evening

published on
Previous
If it quacks like ...