Skip to content

Commit 51a1639

Browse files
beatbrotjlfwongCopilot
authored
Basic Java Flight Recorder support (#533)
* Basic Java Flight Recorder support * Minor touchups * Move jfrview to dev dependencies * Add tests for JFR parsing I needed quite a bit of setup to get this working. I am wondering whether this is worth the effort... * Simplify test setup * Fix accidently changes test * Fix typescript checking * Update typescript-json-schema * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update snapshots --------- Co-authored-by: Jamie Wong <jamie.lf.wong@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 40b2fea commit 51a1639

File tree

11 files changed

+5040
-10618
lines changed

11 files changed

+5040
-10618
lines changed

package-lock.json

Lines changed: 3788 additions & 10616 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,12 @@
3939
"acorn": "7.2.0",
4040
"aphrodite": "2.1.0",
4141
"esbuild": "0.27.0",
42+
"esbuild-jest": "0.5.0",
4243
"eslint": "8.0.0",
4344
"eslint-plugin-prettier": "5.1.2",
4445
"eslint-plugin-react-hooks": "4.6.0",
4546
"jest": "24.3.0",
47+
"jfrview": "0.2.0",
4648
"jsverify": "0.8.3",
4749
"jszip": "3.1.5",
4850
"pako": "1.0.6",
@@ -53,17 +55,22 @@
5355
"ts-jest": "24.3.0",
5456
"tsx": "4.19.2",
5557
"typescript": "5.3.3",
56-
"typescript-json-schema": "0.42.0",
58+
"typescript-json-schema": "0.67.0",
5759
"uglify-es": "3.2.2",
5860
"uint8array-json-parser": "0.0.2"
5961
},
6062
"jest": {
6163
"transform": {
62-
"^.+\\.tsx?$": "ts-jest"
64+
"^.+\\.tsx?$": "ts-jest",
65+
"^.+\\.js$": "esbuild-jest"
6366
},
67+
"transformIgnorePatterns": [],
6468
"setupFilesAfterEnv": [
6569
"./src/jest-setup.js"
6670
],
71+
"moduleNameMapper": {
72+
"\\jfrview_bg.wasm$": "<rootDir>/src/import/java-flight-record.mock.ts"
73+
},
6774
"testRegex": "\\.test\\.tsx?$",
6875
"collectCoverageFrom": [
6976
"**/*.{ts,tsx}",
347 KB
Binary file not shown.

scripts/esbuild-shared.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const buildOptions: esbuild.BuildOptions = {
2626
'.woff2': 'file',
2727
'.png': 'file',
2828
'.ico': 'file',
29+
'.wasm': 'file',
2930
},
3031
}
3132

src/global.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
declare global {
2+
// Mock modern Typescript constructs used in JfrView until we update our Typescript
3+
interface SymbolConstructor {
4+
readonly dispose: symbol
5+
}
6+
}
7+
8+
export {}

src/import/__snapshots__/java-flight-recorder.test.ts.snap

Lines changed: 1167 additions & 0 deletions
Large diffs are not rendered by default.

src/import/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {isTraceEventFormatted, importTraceEvents} from './trace-event'
2424
import {importFromCallgrind} from './callgrind'
2525
import {importFromPapyrus} from './papyrus'
2626
import {importFromPMCStatCallGraph} from './pmcstat-callgraph'
27+
import {importFromJfr, isJfrRecording} from './java-flight-recorder'
2728

2829
export async function importProfileGroupFromText(
2930
fileName: string,
@@ -127,6 +128,9 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
127128
} else if (fileName.endsWith('.pmcstat.graph')) {
128129
console.log('Importing as pmcstat callgraph format')
129130
return toGroup(importFromPMCStatCallGraph(contents))
131+
} else if (fileName.endsWith('.jfr')) {
132+
console.log('Importing as Java Flight Recorder profile')
133+
return await importFromJfr(fileName, buffer)
130134
}
131135

132136
// Second pass: Try to guess what file format it is based on structure
@@ -197,6 +201,11 @@ async function _importProfileGroup(dataSource: ProfileDataSource): Promise<Profi
197201
return toGroup(importFromPapyrus(contents))
198202
}
199203

204+
if (isJfrRecording(buffer)) {
205+
console.log('Importing as Java Flight Recorder profile')
206+
return importFromJfr(fileName, buffer)
207+
}
208+
200209
const fromLinuxPerf = importFromLinuxPerf(contents)
201210
if (fromLinuxPerf) {
202211
console.log('Importing from linux perf script output')
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import fs from 'fs'
2+
3+
// We need to load the wasm binary as file
4+
// This is different from the production case where we load the binary via `fetch`
5+
const wasm_module = fs.readFileSync('node_modules/jfrview/jfrview_bg.wasm')
6+
module.exports = wasm_module
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import {checkProfileSnapshot} from '../lib/test-utils'
2+
3+
test('importFromJfr', async () => {
4+
await checkProfileSnapshot('./sample/profiles/java-flight-recorder/heavy.jfr')
5+
})

src/import/java-flight-recorder.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import {FrameInfo, Profile, ProfileGroup, StackListProfileBuilder} from '../lib/profile'
2+
import init, {Frame, interpret_jfr} from 'jfrview'
3+
// @ts-ignore
4+
import wasm_data from 'jfrview/jfrview_bg.wasm'
5+
6+
export async function importFromJfr(fileName: string, data: ArrayBuffer): Promise<ProfileGroup> {
7+
await init({module_or_path: wasm_data})
8+
const withoutNative = create_profile(data, false)
9+
const withNative = create_profile(data, true)
10+
11+
return {
12+
indexToView: 0,
13+
name: fileName,
14+
profiles: [withoutNative, withNative],
15+
}
16+
}
17+
18+
export function isJfrRecording(buffer: ArrayBuffer) {
19+
const b = buffer.slice(0, 3)
20+
const bytes = new Uint8Array(b)
21+
return bytes[0] === 0x46 && bytes[1] === 0x4c && bytes[2] === 0x52
22+
}
23+
24+
function create_profile(data: ArrayBuffer, includeNative: boolean): Profile {
25+
const result = interpret_jfr(new Uint8Array(data), includeNative)
26+
27+
const builder = new StackListProfileBuilder(result.length)
28+
if (includeNative) {
29+
builder.setName('With native calls')
30+
} else {
31+
builder.setName('Without native calls')
32+
}
33+
34+
function to_fi(input: Frame): FrameInfo {
35+
return {
36+
name: input.name,
37+
key: input.name,
38+
}
39+
}
40+
41+
for (const sample of result) {
42+
builder.appendSampleWithWeight(sample.frames.map(to_fi), 1)
43+
}
44+
45+
return builder.build()
46+
}

0 commit comments

Comments
 (0)