// Copyright 2019 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as fs from 'fs'; import * as path from 'path'; import { Root } from 'protobufjs'; // Requirements: node 10.4.0+, npm // Setup: // (nvm is optional, you can also just install node manually) // $ nvm use // $ npm install // $ npm run build // Usage: node proto-to-json.js path_to_trace.proto input_file output_file // Converts a binary proto file to a 'Trace Event Format' compatible .json file // that can be used with chrome://tracing. Documentation of this format: // https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU // Attempts to reproduce the logic of the JSONTraceWriter in V8 in terms of the // JSON fields it will include/exclude based on the data present in the trace // event. // Convert a string representing an int or uint (64 bit) to a Number or throw // if the value won't fit. function parseIntOrThrow(int: string) { if (BigInt(int) > Number.MAX_SAFE_INTEGER) { throw new Error("Loss of int precision"); } return Number(int); } function uint64AsHexString(val : string) : string { return "0x" + BigInt(val).toString(16); } function parseArgValue(arg: any) : any { if (arg.jsonValue) { return JSON.parse(arg.jsonValue); } if (typeof arg.stringValue !== 'undefined') { return arg.stringValue; } if (typeof arg.uintValue !== 'undefined') { return parseIntOrThrow(arg.uintValue); } if (typeof arg.intValue !== 'undefined') { return parseIntOrThrow(arg.intValue); } if (typeof arg.boolValue !== 'undefined') { return arg.boolValue; } if (typeof arg.doubleValue !== 'undefined') { // Handle [-]Infinity and NaN which protobufjs outputs as strings here. return typeof arg.doubleValue === 'string' ? arg.doubleValue : Number(arg.doubleValue); } if (typeof arg.pointerValue !== 'undefined') { return uint64AsHexString(arg.pointerValue); } } // These come from // https://cs.chromium.org/chromium/src/base/trace_event/common/trace_event_common.h const TRACE_EVENT_FLAG_HAS_ID: number = 1 << 1; const TRACE_EVENT_FLAG_FLOW_IN: number = 1 << 8; const TRACE_EVENT_FLAG_FLOW_OUT: number = 1 << 9; async function main() { const root = new Root(); const { resolvePath } = root; const numDirectoriesToStrip = 2; let initialOrigin: string|null; root.resolvePath = (origin, target) => { if (!origin) { initialOrigin = target; for (let i = 0; i <= numDirectoriesToStrip; i++) { initialOrigin = path.dirname(initialOrigin); } return resolvePath(origin, target); } return path.resolve(initialOrigin!, target); }; const traceProto = await root.load(process.argv[2]); const Trace = traceProto.lookupType("Trace"); const payload = await fs.promises.readFile(process.argv[3]); const msg = Trace.decode(payload).toJSON(); const output = { traceEvents: msg.packet .filter((packet: any) => !!packet.chromeEvents) .map((packet: any) => packet.chromeEvents.traceEvents) .map((traceEvents: any) => traceEvents.map((e: any) => { const bind_id = (e.flags & (TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT)) ? e.bindId : undefined; const scope = (e.flags & TRACE_EVENT_FLAG_HAS_ID) && e.scope ? e.scope : undefined; return { pid: e.processId, tid: e.threadId, ts: parseIntOrThrow(e.timestamp), tts: parseIntOrThrow(e.threadTimestamp), ph: String.fromCodePoint(e.phase), cat: e.categoryGroupName, name: e.name, dur: parseIntOrThrow(e.duration), tdur: parseIntOrThrow(e.threadDuration), bind_id: bind_id, flow_in: e.flags & TRACE_EVENT_FLAG_FLOW_IN ? true : undefined, flow_out: e.flags & TRACE_EVENT_FLAG_FLOW_OUT ? true : undefined, scope: scope, id: (e.flags & TRACE_EVENT_FLAG_HAS_ID) ? uint64AsHexString(e.id) : undefined, args: (e.args || []).reduce((js_args: any, proto_arg: any) => { js_args[proto_arg.name] = parseArgValue(proto_arg); return js_args; }, {}) }; })) .flat() }; await fs.promises.writeFile(process.argv[4], JSON.stringify(output, null, 2)); } main().catch(console.error);