blob: dcc93017bf766a9889051317e17a1d7d1ab77c7a [file]
// Copyright 2019 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.
'use strict';
importScripts('./shared.js');
importScripts('./caspian_web.js');
const LoadWasm = new Promise(function(resolve, reject) {
Module['onRuntimeInitialized'] = function() {
console.log('Loaded WebAssembly runtime');
resolve();
}
});
const _PATH_SEP = '/';
const _NAMES_TO_FLAGS = Object.freeze({
hot: _FLAGS.HOT,
generated: _FLAGS.GENERATED_SOURCE,
coverage: _FLAGS.COVERAGE,
uncompressed: _FLAGS.UNCOMPRESSED,
});
/**
* Wrapper around fetch for requesting the same resource multiple times.
*/
class DataFetcher {
constructor(input) {
/** @type {AbortController | null} */
this._controller = null;
this.setInput(input);
}
/**
* Sets the input that describes what will be fetched. Also clears the cache.
* @param {string | Request} input URL to the resource you want to fetch.
*/
setInput(input) {
if (typeof this._input === 'string' && this._input.startsWith('blob:')) {
// Revoke the previous Blob url to prevent memory leaks
URL.revokeObjectURL(this._input);
}
/** @type {Uint8Array | null} */
this._cache = null;
this._input = input;
}
/**
* Starts a new request and aborts the previous one.
* @param {string | Request} url
*/
async fetch(url) {
if (this._controller) this._controller.abort();
this._controller = new AbortController();
const headers = new Headers();
headers.append('cache-control', 'no-cache');
return fetch(url, {
headers,
credentials: 'same-origin',
signal: this._controller.signal,
});
}
/**
* Outputs a single UInt8Array containing the entire input .size file.
*/
async loadSizeBuffer() {
if (!this._cache) {
const response = await this.fetch(this._input);
this._cache = new Uint8Array(await response.arrayBuffer());
}
return this._cache;
}
}
function mallocBuffer(buf) {
var dataPtr = Module._malloc(buf.byteLength);
var dataHeap = new Uint8Array(Module.HEAPU8.buffer, dataPtr, buf.byteLength);
dataHeap.set(new Uint8Array(buf));
return dataHeap;
}
async function Open(name) {
return LoadWasm.then(() => {
_Open = Module.cwrap('Open', 'number', ['string']);
const stringPtr = _Open(name);
// Something has gone wrong if we get back a string longer than 67MB.
const ret = JSON.parse(Module.UTF8ToString(stringPtr, 2 ** 26));
return ret;
});
}
// Placeholder input name until supplied via setInput()
const fetcher = new DataFetcher('data.ndjson');
let beforeFetcher = null;
let sizeFileLoaded = false;
async function loadSizeFile(isBefore, fetcher) {
const sizeBuffer = await fetcher.loadSizeBuffer();
const heapBuffer = mallocBuffer(sizeBuffer);
const LoadSizeFile = Module.cwrap(
isBefore ? 'LoadBeforeSizeFile' : 'LoadSizeFile', 'bool',
['number', 'number']);
const start_time = Date.now();
LoadSizeFile(heapBuffer.byteOffset, sizeBuffer.byteLength);
console.log(
'Loaded size file in ' + (Date.now() - start_time) / 1000.0 + ' seconds');
Module._free(heapBuffer.byteOffset);
}
async function buildTree(
groupBy, includeRegex, excludeRegex, includeSections, minSymbolSize,
flagToFilter, methodCountMode, onProgress) {
onProgress({percent: 0.1, id: 0});
return await LoadWasm.then(async () => {
if (!sizeFileLoaded) {
const current = loadSizeFile(false, fetcher);
const before =
beforeFetcher !== null ? loadSizeFile(true, beforeFetcher) : null;
await current;
await before;
onProgress({percent: 0.4, id: 0});
sizeFileLoaded = true;
}
const BuildTree = Module.cwrap(
'BuildTree', 'bool',
['bool', 'string', 'string', 'string', 'string', 'number', 'number']);
const start_time = Date.now();
const diffMode = BuildTree(
methodCountMode, groupBy, includeRegex, excludeRegex,
includeSections, minSymbolSize, flagToFilter);
console.log(
'Constructed tree in ' + (Date.now() - start_time) / 1000.0 +
' seconds');
onProgress({percent: 0.8, id: 0});
const root = await Open('');
return {
root,
percent: 1.0,
diffMode,
};
});
}
/**
* Parse the options represented as a query string, into an object.
* Includes checks for valid values.
* @param {string} options Query string
*/
function parseOptions(options) {
const params = new URLSearchParams(options);
const groupBy = params.get('group_by') || 'source_path';
const methodCountMode = params.has('method_count');
const includeRegex = params.get('include');
const excludeRegex = params.get('exclude');
let includeSections = params.get('type');
if (methodCountMode) {
includeSections = _DEX_METHOD_SYMBOL_TYPE;
} else if (includeSections === null) {
// Exclude native symbols by default.
let includeSectionsSet = new Set(_SYMBOL_TYPE_SET);
includeSectionsSet.delete('b');
includeSections = Array.from(includeSectionsSet.values()).join('');
}
const minSymbolSize = Number(params.get('min_size'));
if (Number.isNaN(minSymbolSize)) {
minSymbolSize = 0;
}
const flagToFilter = _NAMES_TO_FLAGS[params.get('flag_filter')] || 0;
const url = params.get('load_url');
const beforeUrl = params.get('before_url');
return {
groupBy,
includeRegex,
excludeRegex,
includeSections,
minSymbolSize,
flagToFilter,
methodCountMode,
url,
beforeUrl,
};
}
const actions = {
/** @param {{input:string|null,options:string}} param0 */
load({input, options}) {
const {
groupBy,
includeRegex,
excludeRegex,
includeSections,
minSymbolSize,
flagToFilter,
methodCountMode,
url,
beforeUrl,
} = parseOptions(options);
if (input === 'from-url://' && url) {
// Display the data from the `load_url` query parameter
console.info('Displaying data from', url);
fetcher.setInput(url);
} else if (input != null) {
console.info('Displaying uploaded data');
fetcher.setInput(input);
}
if (beforeUrl) {
beforeFetcher = new DataFetcher(beforeUrl);
}
return buildTree(
groupBy, includeRegex, excludeRegex, includeSections, minSymbolSize,
flagToFilter, methodCountMode, progress => {
// @ts-ignore
self.postMessage(progress);
});
},
/** @param {string} path */
async open(path) {
return Open(path);
},
};
/**
* Call the requested action function with the given data. If an error is thrown
* or rejected, post the error message to the UI thread.
* @param {number} id Unique message ID.
* @param {string} action Action type, corresponding to a key in `actions.`
* @param {any} data Data to supply to the action function.
*/
async function runAction(id, action, data) {
try {
const result = await actions[action](data);
// @ts-ignore
self.postMessage({id, result});
} catch (err) {
// @ts-ignore
self.postMessage({id, error: err.message});
throw err;
}
}
const runActionDebounced = debounce(runAction, 0);
/**
* @param {MessageEvent} event Event for when this worker receives a message.
*/
self.onmessage = async event => {
const {id, action, data} = event.data;
if (action === 'load') {
// Loading large files will block the worker thread until complete or when
// an await statement is reached. During this time, multiple load messages
// can pile up due to filters being adjusted. We debounce the load call
// so that only the last message is read (the current set of filters).
runActionDebounced(id, action, data);
} else {
runAction(id, action, data);
}
};