Index: tracing/tracing/extras/importer/profiling_dictionary_reader.html |
diff --git a/tracing/tracing/extras/importer/profiling_dictionary_reader.html b/tracing/tracing/extras/importer/profiling_dictionary_reader.html |
new file mode 100644 |
index 0000000000000000000000000000000000000000..ae8c51334e8fd6ed57f321425b6b35056b8f8e60 |
--- /dev/null |
+++ b/tracing/tracing/extras/importer/profiling_dictionary_reader.html |
@@ -0,0 +1,315 @@ |
+<!DOCTYPE html> |
+<!-- |
+Copyright 2017 The Chromium Authors. All rights reserved. |
+Use of this source code is governed by a BSD-style license that can be |
+found in the LICENSE file. |
+--> |
+ |
+<link rel="import" href="/tracing/base/base.html"> |
+ |
+<script> |
+'use strict'; |
+ |
+tr.exportTo('tr.e.importer', function() { |
+ const STRING_ID_SUFFIX = '_sid'; |
+ const PLURAL_STRING_ID_SUFFIX = '_sids'; |
+ |
+ function isStringReference(s) { |
+ return s.endsWith(STRING_ID_SUFFIX) || s.endsWith(PLURAL_STRING_ID_SUFFIX); |
+ } |
+ |
+ function getStringReferenceName(name) { |
+ if (name.endsWith(PLURAL_STRING_ID_SUFFIX)) { |
+ return name.slice(0, -PLURAL_STRING_ID_SUFFIX.length); |
+ } |
+ return name.slice(0, -STRING_ID_SUFFIX.length); |
+ } |
+ |
+ function deferenceStrings(idToString, o) { |
+ const clone = Object.assign({}, o); |
+ for (const [key, value] of Object.entries(clone)) { |
+ if (isStringReference(key)) { |
+ const name = getStringReferenceName(key); |
+ clone[name] = idToString(value); |
+ } |
+ } |
+ return clone; |
+ } |
+ |
+ function singularize(word) { |
+ if (word.endsWith('s')) { |
+ return word.slice(0, -1); |
+ } |
+ return word; |
+ } |
+ |
+ function getMetadataPairs(dataJson) { |
+ const isMetadata = v => typeof v !== 'object' || Array.isArray(v); |
+ const pairs = Object.entries(dataJson); |
+ const metadataPairs = pairs.filter(([_, v]) => isMetadata(v)); |
+ return metadataPairs; |
+ } |
+ |
+ function getGroupPairs(dataJson) { |
+ const pairs = Object.entries(dataJson); |
+ const nonMapPairs = pairs.filter(([k, _]) => k !== 'maps'); |
+ const groupPairs = nonMapPairs.filter(([_, v]) => typeof v === 'object'); |
+ return groupPairs; |
+ } |
+ |
+ function createMap(mapJson) { |
+ const map = new Map(); |
+ for (const entry of mapJson) { |
+ if (entry.id === undefined) { |
+ throw new Error('Missing required key "id" in streaming event.'); |
+ } |
+ map.set(entry.id, entry); |
+ } |
+ return map; |
+ } |
+ |
+ function createMaps(mapsJson) { |
+ const maps = new Map(); |
+ for (const [name, mapJson] of Object.entries(mapsJson)) { |
+ maps.set(name, createMap(mapJson)); |
+ } |
+ return maps; |
+ } |
+ |
+ function createGroup(groupJson, opt_startTime) { |
+ const entries = []; |
+ const n = Object.values(groupJson)[0].length; |
+ |
+ for (let i = 0; i < n; i++) { |
+ const entry = {}; |
+ for (const name in groupJson) { |
+ entry[name] = groupJson[name][i]; |
+ } |
+ entries.push(entry); |
+ } |
+ |
+ const timeDelta = groupJson.timeDelta; |
+ if (opt_startTime === undefined && timeDelta !== undefined) { |
+ throw new Error('Missing required key "startTime" in streaming event.'); |
+ } |
+ |
+ if (opt_startTime) { |
+ let delta = 0; |
+ for (const entry of entries) { |
+ delta += entry.timeDelta ? entry.timeDelta : 0; |
+ entry.time = opt_startTime + delta; |
+ } |
+ } |
+ |
+ return entries; |
+ } |
+ |
+ function createGroups(groupsJson, opt_startTime) { |
+ const groups = new Map(); |
+ for (const [name, groupJson] of Object.entries(groupsJson)) { |
+ groups.set(name, createGroup(groupJson, opt_startTime)); |
+ } |
+ |
+ return groups; |
+ } |
+ |
+ function createMetadata(metadataPairs) { |
+ const metadata = new Map(); |
+ for (const [name, value] of metadataPairs) { |
+ metadata.set(name, value); |
+ } |
+ if (metadata.get('version') === undefined) { |
+ throw new Error('Missing required key "version" in streaming event.'); |
+ } |
+ return metadata; |
+ } |
+ |
+ /** |
+ * Extracts data from a profiling dictionary. See goo.gl/R0Ae4f. |
+ * |
+ * A profiling dictionary is a compressed format that is good for recording |
+ * sampling data. ProfilingDictionaryReader unpacks that data. To use the |
+ * ProfilingDictionaryReader first create an 'empty' reader using .empty() |
+ * then call #expandData(data) on your dictionary or the helper: |
+ * #expandEvent(event) on a tracing event containing the profiling dictionary. |
+ * ProfilingDictionaryReader is an immutable data structure so these methods |
+ * don't modify the ProfilingDictionaryReader instead they return new |
+ * ProfilingDictionaryReaders which wrap the data you passed. To access the |
+ * unpacked data use the #inflated property and the #getMapValue() method. |
+ * |
+ * Usage example, given input like: |
+ * $ let input = { |
+ * version: 1, |
+ * allocators: { |
+ * books: { |
+ * authors: [1, 1, 2], |
+ * title_sid: [10, 11, 12], |
+ * }, |
+ * }, |
+ * maps: { |
+ * authors: [ |
+ * { id: 1, name_sid: 1 }, |
+ * { id: 2, name_sid: 2 }, |
+ * ], |
+ * strings: [ |
+ * { id: 1, string: 'DFW' }, |
+ * { id: 2, string: 'C. Stross' }, |
+ * { id: 10, string: 'Book A' }, |
+ * { id: 11, string: 'Book B' }, |
+ * { id: 12, string: 'Book C' }, |
+ * ], |
+ * } |
+ * }; |
+ * We can create an empty reader: |
+ * $ let reader = ProfilingDictionaryReader.empty(); |
+ * Then read in the input: |
+ * $ reader = reader.expandData(input); |
+ * Then view the expanded data: |
+ * $ console.log(reader.inflated); |
+ * { |
+ * books: [ |
+ * { author: { id: 1, name: 'DFW' }, title: "Book A", }, |
+ * { author: { id: 2, name: 'C. Stross' }, title: "Book B", }, |
+ * { author: { id: 2, name: 'C. Stross' }, title: "Book C", }, |
+ * ], |
+ * } |
+ * |
+ */ |
+ class ProfilingDictionaryReader { |
+ constructor(opt_metadata, opt_maps, opt_groups, opt_parent) { |
+ this.metadata = opt_metadata || new Map(); |
+ this.maps = opt_maps || new Map(); |
+ this.groups = opt_groups || new Map(); |
+ this.parent_ = opt_parent || undefined; |
+ this.inflated_ = undefined; |
+ this.raw_ = undefined; |
+ this.boundGetString_ = this.getString.bind(this); |
+ this.deferenceStrings_ = o => deferenceStrings(this.boundGetString_, o); |
+ } |
+ |
+ /** |
+ * Creates an empty ProfilingDictionaryReader. |
+ */ |
+ static empty() { |
+ return new ProfilingDictionaryReader(); |
+ } |
+ |
+ /** |
+ * Returns the parent or null if this is the root ProfilingDictionaryReader. |
+ */ |
+ get parent() { |
+ return this.parent_; |
+ } |
+ |
+ get raw() { |
+ if (this.raw_) return this.raw_; |
+ this.raw_ = {}; |
+ for (const [name, group] of this.groups.entries()) { |
+ this.raw_[name] = group; |
+ } |
+ return this.raw_; |
+ } |
+ |
+ get inflated() { |
+ if (this.inflated_) return this.inflated_; |
+ this.inflated_ = {}; |
+ for (const [name, group] of this.groups.entries()) { |
+ this.inflated_[name] = this.inflateGroup(group); |
+ } |
+ return this.inflated_; |
+ } |
+ |
+ /** |
+ * Get a map from the newest event by name. |
+ * If no map with that name was present returns an empty Map. |
+ */ |
+ getNewMap(name) { |
+ return this.maps.get(name) || new Map(); |
+ } |
+ |
+ /** |
+ * Get a record with the id |id| from the map with name |mapName|. |
+ * This method searches through the expanded events in reverse order of |
+ * expansion until it finds a matching value. If no value matches returns |
+ * undefined. |
+ */ |
+ getMapValue(mapName, id) { |
+ let value = this.getNewMap(mapName).get(id); |
+ if (value === undefined && this.parent) { |
+ value = this.parent.getMapValue(mapName, id); |
+ } |
+ return value; |
+ } |
+ |
+ /** |
+ * Get the string with the id |id|. |
+ * This method searches through the expanded events in reverse order of |
+ * expansion until it finds a string with the matching id. If there is no |
+ * matching string with returns undefined. |
+ */ |
+ getString(id) { |
+ const value = this.getMapValue('strings', id); |
+ if (value === undefined) return undefined; |
+ return value.string; |
+ } |
+ |
+ /** |
+ * True iff this or any parent has a map with name |name|. |
+ */ |
+ hasMap(name) { |
+ if (this.maps.has(name)) return true; |
+ if (this.parent === undefined) return false; |
+ return this.parent.hasMap(name); |
+ } |
+ |
+ inflateGroup(group) { |
+ return group.map(this.inflateEntry.bind(this)); |
+ } |
+ |
+ inflateEntry(entry) { |
+ const inflatedEntry = {}; |
+ for (const [name, value] of Object.entries(entry)) { |
+ let inflatedValue; |
+ if (this.hasMap(name)) { |
+ const id = value; |
+ inflatedValue = this.deferenceStrings_(this.getMapValue(name, id)); |
+ } else { |
+ inflatedValue = value; |
+ } |
+ inflatedEntry[singularize(name)] = inflatedValue; |
+ } |
+ return this.deferenceStrings_(inflatedEntry); |
+ } |
+ |
+ /** |
+ * Returns a new ProfilingDictionaryReader with this |
+ * ProfilingDictionaryReader as its parent and the fields 'maps', 'groups' |
+ * and 'metadata' filled in based on |data|. |
+ */ |
+ expandData(data) { |
+ const mapsJson = data.maps || {}; |
+ const groupsJson = data.allocators || {}; |
+ const metadataPairs = getMetadataPairs(data); |
+ const metadata = createMetadata(metadataPairs); |
+ const opt_startTime = metadata.get('startTime'); |
+ const maps = createMaps(mapsJson); |
+ const groups = createGroups(groupsJson, opt_startTime); |
+ return new ProfilingDictionaryReader(metadata, maps, groups, this); |
+ } |
+ |
+ /** |
+ * Convenience method for this.expandData(event.args.data). |
+ */ |
+ expandEvent(event) { |
+ return this.expandData(event.args.data); |
+ } |
+ } |
+ |
+ return { |
+ ProfilingDictionaryReader, |
+ singularize, |
+ deferenceStringsForTest: deferenceStrings, |
+ }; |
+}); |
+</script> |
+ |