/translate.rb Secret
Created
July 8, 2023 22:32
Translate stackprof profiles to FireFox profiler JSON
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Translate Stackprof profiles to FireFox compatible JSON | |
class FFTranslate | |
def translate raw_profile | |
strings = Hash.new { |h, k| h[k] = h.size } | |
start_time = raw_profile[:raw_sample_timestamps].first / 1000.0 | |
end_time = raw_profile[:raw_sample_timestamps].last / 1000.0 | |
profile = _translate(raw_profile) | |
{ | |
"meta" => { | |
"interval" => raw_profile[:interval] / 1000, # stackprof is in usec, but ff wants ms | |
"startTime" => start_time, | |
"endTime" => end_time, | |
"processType" => 0, | |
"product" => "Ruby", | |
"stackwalk" => 1, | |
"version" => 28, | |
"preprocessedProfileVersion" => 47, | |
"symbolicated" => true, | |
"markerSchema" => [], | |
"sampleUnits" => { | |
"time" => "ms", | |
"eventDelay" => "ms", | |
"threadCPUDelta" => "µs" | |
} | |
}, | |
"libs" => [], | |
"threads" => [ | |
{ | |
"name" => "Main", | |
"isMainThread" => true, | |
"processStartupTime" => start_time, | |
"processShutdownTime" => end_time, | |
"registerTime" => 0, | |
"unregisterTime" => nil, | |
"pausedRanges" => [], | |
"pid" => 123, | |
"tid" => 456, | |
"funcTable" => profile.func_table(strings), | |
"frameTable" => profile.frame_table(strings), | |
"stackTable" => profile.stack_table(strings), | |
"samples" => profile.samples_table(strings), | |
"nativeSymbols" => {}, | |
"resourceTable" => { | |
"lenght" => 0, | |
"lib" => [], | |
"name" => [], | |
"host" => [], | |
"type" => [] | |
}, | |
"markers" => { | |
"data" => [], | |
"name" => [], | |
"startTime" => [], | |
"endTime" => [], | |
"phase" => [], | |
"category" => [], | |
"length" => 0 | |
}, | |
"stringArray" => strings.keys | |
} | |
], | |
} | |
end | |
private | |
Function = Struct.new(:addr, :name, :filename, :line, :column, :func_table_index) | |
class Frame | |
attr_reader :addr, :line, :function, :callees | |
attr_accessor :frame_table_index, :stack_table_index | |
def initialize addr, line, function, callees, frame_table_index, stack_table_index | |
@addr = addr | |
@line = line | |
@function = function | |
@callees = callees | |
@frame_table_index = frame_table_index | |
@stack_table_index = stack_table_index | |
end | |
def hash | |
[addr, line].hash | |
end | |
def == other | |
addr == other.addr && line == other.line | |
end | |
alias :eql? :== | |
end | |
class Profile < Struct.new(:functions, :frames, :root_frame, :samples, :sample_times) | |
def samples_table strings | |
raise unless samples.length == sample_times.length | |
{ | |
"stack" => samples.map(&:stack_table_index), | |
"time" => sample_times.map { |time| time / 1000.0 }, | |
"weight" => nil, | |
"weightType" => "samples", | |
"length" => samples.length | |
} | |
end | |
def frame_table strings | |
len = frames.length | |
none = [nil] * len | |
frames.each_with_index { |frame, i| frame.frame_table_index = i } | |
{ | |
"address" => [-1] * len, | |
"inlineDepth" => [0] * len, | |
"category" => nil, | |
"subcategory" => nil, | |
"func" => frames.map { |frame| frame.function.func_table_index }, | |
"nativeSymbol" => none, | |
"innerWindowID" => none, | |
"implementation" => none, | |
"line" => frames.map(&:line), | |
"column" => none, | |
"length" => len | |
} | |
end | |
def func_table strings | |
functions.each_with_index { |func, i| func.func_table_index = i } | |
{ | |
"name" => functions.map { strings[_1.name] }, | |
"isJS" => [false] * functions.length, | |
"relevantForJS" => [false] * functions.length, | |
"resource" => [-1] * functions.length, # set to unidentified for now | |
"fileName" => functions.map { strings[_1.filename] }, | |
"lineNumber" => functions.map { _1.line }, | |
"columnNumber" => functions.map { _1.column }, | |
"length" => functions.length | |
} | |
end | |
def stack_table strings | |
frame_table_indicies = [] | |
prefixes = [] | |
root_frame.stack_table_index = 0 | |
prefixes << nil | |
frame_table_indicies << (root_frame.frame_table_index || raise) | |
stack = [root_frame] | |
seen = Set.new | |
while parent_frame = stack.pop | |
next if seen.include?(parent_frame) | |
seen << parent_frame | |
parent_frame.callees.each do |frame| | |
frame.stack_table_index = prefixes.length | |
prefixes << parent_frame.stack_table_index | |
frame_table_indicies << (frame.frame_table_index || raise) | |
stack.push frame | |
end | |
end | |
raise unless prefixes.length == frame_table_indicies.length | |
{ | |
"frame" => frame_table_indicies, | |
"category" => [1] * prefixes.length, | |
"subcategory" => [0] * prefixes.length, | |
"prefix" => prefixes, | |
"length" => prefixes.length | |
} | |
end | |
end | |
def _translate profile | |
stacks = profile[:raw] | |
lines = profile[:raw_lines] | |
functions = {} | |
frames = {} | |
root_frame = nil | |
samples = [] | |
time_stamps = [] | |
i = 0 | |
gc = 0 | |
sample_idx = 0 | |
while i < stacks.length | |
len = stacks[i] | |
caller_frame = nil | |
if stacks[i + 1] == 1 # This is a GC sample | |
# For now we'll skip GC samples | |
gc += stacks[i + len + 1] | |
sample_idx += stacks[i + len + 1] | |
else | |
stacks[i + 1, len].zip(lines[i + 1, len]).each do |addr, line| | |
function = functions[addr] ||= begin | |
info = profile[:frames][addr] | |
Function.new(addr, info[:name], info[:file], info[:line], nil) | |
end | |
frame = frames[[addr, line]] ||= Frame.new(addr, line, function, Set.new, nil, nil) | |
if caller_frame | |
caller_frame.callees << frame | |
else | |
root_frame = frame | |
end | |
caller_frame = frame | |
end | |
count = stacks[i + len + 1] | |
count.times { |j| | |
samples << caller_frame | |
time_stamps << profile[:raw_sample_timestamps][sample_idx + j] | |
} | |
sample_idx += count | |
end | |
i += (len + 2) | |
end | |
# Make sure the math adds up. We always record a time stamp, even on GC | |
raise unless (gc + samples.length) == profile[:raw_sample_timestamps].length | |
raise unless sample_idx == profile[:raw_sample_timestamps].length | |
Profile.new(functions.values, frames.values, root_frame, samples, time_stamps) | |
end | |
end | |
profile = Marshal.load ARGF.read | |
translator = FFTranslate.new | |
require "json" | |
puts JSON.dump(translator.translate(profile)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment