class Vernier::Output::Firefox::Thread
def allocations_table
def allocations_table return nil if !@allocations samples, weights, timestamps = @allocations.values_at(:samples, :weights, :timestamps) return nil if samples.size == 0 size = samples.size timestamps = timestamps.map { _1 / 1_000_000.0 } ret = { "time": timestamps, "className": ["Object"]*size, "typeName": ["JSObject"]*size, "coarseType": ["Object"]*size, "weight": weights, "inNursery": [false] * size, "stack": samples, "length": size } ret end
def data
def data started_at = (@started_at - 0) / 1_000_000.0 stopped_at = (@stopped_at - 0) / 1_000_000.0 if @stopped_at { name: @name, isMainThread: @is_main, processStartupTime: started_at, processShutdownTime: stopped_at, registerTime: started_at, unregisterTime: stopped_at, pausedRanges: [], pid: profile.pid || Process.pid, tid: @tid, frameTable: frame_table, funcTable: func_table, nativeSymbols: {}, samples: samples_table, jsAllocations: allocations_table, stackTable: stack_table, resourceTable: { length: 0, lib: [], name: [], host: [], type: [] }, markers: markers_table, stringArray: string_table }.compact end
def filter_filenames(filenames)
def filter_filenames(filenames) filter = FilenameFilter.new filenames.map do |filename| filter.call(filename) end end
def frame_table
def frame_table funcs = @stack_table_hash[:frame_table].fetch(:func) lines = @stack_table_hash[:frame_table].fetch(:line) raise unless lines.size == funcs.size size = funcs.size none = [nil] * size default = [0] * size unidentified = [-1] * size categories = @frame_categories.map(&:idx) subcategories = @frame_subcategories { address: unidentified, inlineDepth: default, category: categories, subcategory: subcategories, func: funcs, nativeSymbol: none, innerWindowID: none, implementation: @frame_implementations, line: lines, column: none, length: size } end
def func_table
def func_table size = @func_names.size cfunc_idx = @strings["<cfunc>"] is_js = @filenames.map { |fn| fn != cfunc_idx } line_numbers = @stack_table_hash[:func_table].fetch(:first_line).map.with_index do |line, i| if is_js[i] || line != 0 line else nil end end { name: @func_names, isJS: is_js, relevantForJS: is_js, resource: [-1] * size, # set to unidentified for now fileName: @filenames, lineNumber: line_numbers, columnNumber: [nil] * size, #columnNumber: functions.map { _1.column }, length: size } end
def gc_category
def gc_category @categorizer.get_category("GC") end
def initialize(ruby_thread_id, profile, categorizer, name:, tid:, samples:, weights:, timestamps: nil, sample_categories: nil, markers:, started_at:, stopped_at: nil, allocations: nil, is_main: nil, is_start: nil)
def initialize(ruby_thread_id, profile, categorizer, name:, tid:, samples:, weights:, timestamps: nil, sample_categories: nil, markers:, started_at:, stopped_at: nil, allocations: nil, is_main: nil, is_start: nil) @ruby_thread_id = ruby_thread_id @profile = profile @categorizer = categorizer @tid = tid @name = name @is_main = is_main if is_main.nil? @is_main = @ruby_thread_id == ::Thread.main.object_id end @is_main = true if profile.threads.size == 1 @is_start = is_start.nil? ? @is_main : is_start @stack_table = Vernier::StackTable.new samples = samples.map { |sample| @stack_table.convert(profile._stack_table, sample) } @samples = samples if allocations allocation_samples = allocations[:samples].dup allocation_samples.map! do |sample| @stack_table.convert(profile._stack_table, sample) end allocations = allocations.merge(samples: allocation_samples) end @allocations = allocations timestamps ||= [0] * samples.size @weights, @timestamps = weights, timestamps @sample_categories = sample_categories || ([0] * samples.size) @markers = markers.map do |marker| if stack_idx = marker[5]&.dig(:cause, :stack) marker = marker.dup new_idx = @stack_table.convert(profile._stack_table, stack_idx) marker[5] = marker[5].merge({ cause: { stack: new_idx }}) end marker end @started_at, @stopped_at = started_at, stopped_at @stack_table_hash = @stack_table.to_h names = @stack_table_hash[:func_table].fetch(:name) filenames = @stack_table_hash[:func_table].fetch(:filename) stacks_size = @stack_table.stack_count @categorized_stacks = Hash.new do |h, k| h[k] = h.size + stacks_size end @strings = Hash.new { |h, k| h[k] = h.size } @func_names = names.map do |name| @strings[name] end @filenames = filter_filenames(filenames).map do |filename| @strings[filename] end func_implementations = filenames.map do |filename| # Must match strings in `src/profile-logic/profile-data.js` # inside the firefox profiler. See `getFriendlyStackTypeName` if filename == "<cfunc>" @strings["native"] else # nil means interpreter nil end end @frame_implementations = @stack_table_hash[:frame_table].fetch(:func).map do |func_idx| func_implementations[func_idx] end cfunc_category = @categorizer.get_category("cfunc") ruby_category = @categorizer.get_category("Ruby") func_categories, func_subcategories = [], [] filenames.each do |filename| if filename == "<cfunc>" func_categories << cfunc_category func_subcategories << 0 else func_categories << ruby_category subcategory = ruby_category.subcategories.detect {|c| c.matches?(filename) }&.idx || 0 func_subcategories << subcategory end end @frame_categories = @stack_table_hash[:frame_table].fetch(:func).map do |func_idx| func_categories[func_idx] end @frame_subcategories = @stack_table_hash[:frame_table].fetch(:func).map do |func_idx| func_subcategories[func_idx] end end
def markers_table
def markers_table string_indexes = [] start_times = [] end_times = [] phases = [] categories = [] data = [] @markers.each_with_index do |(_, name, start, finish, phase, datum), i| string_indexes << @strings[name] start_times << (start / 1_000_000.0) # Please don't hate me. Divide by 1,000,000 only if finish is not nil end_times << (finish&./(1_000_000.0)) phases << phase category = if name.start_with?("GC") gc_category.idx elsif name.start_with?("Thread") thread_category.idx else 0 end categories << category data << datum end { data: data, name: string_indexes, startTime: start_times, endTime: end_times, phase: phases, category: categories, length: start_times.size } end
def samples_table
def samples_table samples = @samples weights = @weights categories = @sample_categories size = samples.size if categories.empty? categories = [0] * size end if @timestamps times = @timestamps.map { _1 / 1_000_000.0 } else # FIXME: record timestamps for memory samples times = (0...size).to_a end raise unless weights.size == size raise unless times.size == size samples = samples.zip(categories).map do |sample, category| if category == 0 sample else @categorized_stacks[[sample, category]] end end { stack: samples, time: times, weight: weights, weightType: profile.meta[:mode] == :retained ? "bytes" : "samples", length: samples.length } end
def stack_table
def stack_table frames = @stack_table_hash[:stack_table].fetch(:frame).dup prefixes = @stack_table_hash[:stack_table].fetch(:parent).dup categories = frames.map{|idx| @frame_categories[idx].idx } subcategories = frames.map{|idx| @frame_subcategories[idx] } @categorized_stacks.each_key do |(stack, category)| frames << frames[stack] prefixes << prefixes[stack] categories << category subcategories << 0 end size = frames.length raise unless frames.size == size raise unless prefixes.size == size { frame: frames, category: categories, subcategory: subcategories, prefix: prefixes, length: prefixes.length } end
def string_table
def string_table @strings.keys.map do |string| if string.ascii_only? string elsif string.encoding == Encoding::UTF_8 if string.valid_encoding? string else string.scrub end elsif string.encoding == Encoding::BINARY # TODO: We might want to guess UTF-8 and escape the binary more explicitly string.dup.force_encoding("UTF-8").scrub else # TODO: ideally we should attempt to properly re-encode here, but right now I think this is dead code string.dup.force_encoding("UTF-8").scrub end end end
def thread_category
def thread_category @categorizer.get_category("Thread") end