Skip to content

Instantly share code, notes, and snippets.

@tenderlove
Created July 8, 2023 22:32
Translate stackprof profiles to FireFox profiler JSON
# 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