From a tweet von Gunnar Morling
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 https://github.com/BestSolution-at/jfr-doc if you look at this code keep in mind that it was hacked together in an evening