Skip to content

Commit

Permalink
report: gzip treemap data (#12519)
Browse files Browse the repository at this point in the history
  • Loading branch information
connorjclark authored May 27, 2021
1 parent b31823e commit 0689e4e
Show file tree
Hide file tree
Showing 12 changed files with 231 additions and 53 deletions.
2 changes: 2 additions & 0 deletions build/build-treemap.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,12 @@ async function run() {
fs.readFileSync(require.resolve('tabulator-tables/dist/js/modules/sort.js'), 'utf8'),
fs.readFileSync(require.resolve('tabulator-tables/dist/js/modules/format.js'), 'utf8'),
fs.readFileSync(require.resolve('tabulator-tables/dist/js/modules/resize_columns.js'), 'utf8'),
fs.readFileSync(require.resolve('pako/dist/pako_inflate.js'), 'utf-8'),
/* eslint-enable max-len */
buildStrings(),
{path: '../../lighthouse-core/report/html/renderer/logger.js'},
{path: '../../lighthouse-core/report/html/renderer/i18n.js'},
{path: '../../lighthouse-core/report/html/renderer/text-encoding.js'},
{path: '../../lighthouse-viewer/app/src/drag-and-drop.js'},
{path: '../../lighthouse-viewer/app/src/github-api.js'},
{path: '../../lighthouse-viewer/app/src/firebase-auth.js'},
Expand Down
1 change: 1 addition & 0 deletions lighthouse-core/report/html/html-report-assets.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const REPORT_JAVASCRIPT = [
fs.readFileSync(__dirname + '/renderer/pwa-category-renderer.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/report-renderer.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/i18n.js', 'utf8'),
fs.readFileSync(__dirname + '/renderer/text-encoding.js', 'utf8'),
].join(';\n');
const REPORT_CSS = fs.readFileSync(__dirname + '/report-styles.css', 'utf8');
const REPORT_TEMPLATES = fs.readFileSync(__dirname + '/templates.html', 'utf8');
Expand Down
2 changes: 1 addition & 1 deletion lighthouse-core/report/html/renderer/psi.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ function prepareLabData(LHResult, document) {
container: reportEl.querySelector('.lh-audit-group--metrics'),
text: Util.i18n.strings.viewTreemapLabel,
icon: 'treemap',
onClick: () => ReportUIFeatures.openTreemap(lhResult, 'url'),
onClick: () => ReportUIFeatures.openTreemap(lhResult),
});
}
};
Expand Down
58 changes: 25 additions & 33 deletions lighthouse-core/report/html/renderer/report-ui-features.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
* the report.
*/

/* globals getFilenamePrefix Util ElementScreenshotRenderer */
/* globals getFilenamePrefix Util TextEncoding ElementScreenshotRenderer */

/** @typedef {import('./dom')} DOM */

Expand Down Expand Up @@ -157,8 +157,7 @@ class ReportUIFeatures {
this.addButton({
text: Util.i18n.strings.viewTreemapLabel,
icon: 'treemap',
onClick: () => ReportUIFeatures.openTreemap(
this.json, this._dom.isDevTools() ? 'url' : 'postMessage'),
onClick: () => ReportUIFeatures.openTreemap(this.json),
});
}

Expand Down Expand Up @@ -535,27 +534,34 @@ class ReportUIFeatures {
}

/**
* Opens a new tab to the online viewer and sends the local page's JSON results
* to the online viewer using postMessage.
* The popup's window.name is keyed by version+url+fetchTime, so we reuse/select tabs correctly.
* @param {LH.Result} json
* @protected
*/
static openTabAndSendJsonReportToViewer(json) {
// The popup's window.name is keyed by version+url+fetchTime, so we reuse/select tabs correctly
static computeWindowNameSuffix(json) {
// @ts-ignore - If this is a v2 LHR, use old `generatedTime`.
const fallbackFetchTime = /** @type {string} */ (json.generatedTime);
const fetchTime = json.fetchTime || fallbackFetchTime;
const windowName = `${json.lighthouseVersion}-${json.requestedUrl}-${fetchTime}`;
return `${json.lighthouseVersion}-${json.requestedUrl}-${fetchTime}`;
}

/**
* Opens a new tab to the online viewer and sends the local page's JSON results
* to the online viewer using postMessage.
* @param {LH.Result} json
* @protected
*/
static openTabAndSendJsonReportToViewer(json) {
const windowName = 'viewer-' + this.computeWindowNameSuffix(json);
const url = getAppsOrigin() + '/viewer/';
ReportUIFeatures.openTabAndSendData({lhr: json}, url, windowName);
}

/**
* Opens a new tab to the treemap app and sends the JSON results using postMessage.
* Opens a new tab to the treemap app and sends the JSON results using URL.fragment
* @param {LH.Result} json
* @param {'postMessage'|'url'} method
*/
static openTreemap(json, method = 'postMessage') {
static openTreemap(json) {
const treemapData = json.audits['script-treemap-data'].details;
if (!treemapData) {
throw new Error('no script treemap data found');
Expand All @@ -575,13 +581,9 @@ class ReportUIFeatures {
},
};
const url = getAppsOrigin() + '/treemap/';
const windowName = `treemap-${json.requestedUrl}`;
const windowName = 'treemap-' + this.computeWindowNameSuffix(json);

if (method === 'postMessage') {
ReportUIFeatures.openTabAndSendData(treemapOptions, url, windowName);
} else {
ReportUIFeatures.openTabWithUrlData(treemapOptions, url, windowName);
}
ReportUIFeatures.openTabWithUrlData(treemapOptions, url, windowName);
}

/**
Expand All @@ -607,7 +609,6 @@ class ReportUIFeatures {
}
});

// The popup's window.name is keyed by version+url+fetchTime, so we reuse/select tabs correctly
const popup = window.open(url, windowName);
}

Expand All @@ -618,23 +619,14 @@ class ReportUIFeatures {
* @param {string} windowName
* @protected
*/
static openTabWithUrlData(data, url_, windowName) {
static async openTabWithUrlData(data, url_, windowName) {
const url = new URL(url_);
url.hash = toBinary(JSON.stringify(data));

// The popup's window.name is keyed by version+url+fetchTime, so we reuse/select tabs correctly
const gzip = Boolean(window.CompressionStream);
url.hash = await TextEncoding.toBase64(JSON.stringify(data), {
gzip,
});
if (gzip) url.searchParams.set('gzip', '1');
window.open(url.toString(), windowName);

/**
* @param {string} string
*/
function toBinary(string) {
const codeUnits = new Uint16Array(string.length);
for (let i = 0; i < codeUnits.length; i++) {
codeUnits[i] = string.charCodeAt(i);
}
return btoa(String.fromCharCode(...new Uint8Array(codeUnits.buffer)));
}
}

/**
Expand Down
78 changes: 78 additions & 0 deletions lighthouse-core/report/html/renderer/text-encoding.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
'use strict';

/* global self btoa atob window CompressionStream Response */

const btoa_ = typeof btoa !== 'undefined' ?
btoa :
/** @param {string} str */
(str) => Buffer.from(str).toString('base64');
const atob_ = typeof atob !== 'undefined' ?
atob :
/** @param {string} str */
(str) => Buffer.from(str, 'base64').toString();

/**
* Takes an UTF-8 string and returns a base64 encoded string.
* If gzip is true, the UTF-8 bytes are gzipped before base64'd, using
* CompressionStream (currently only in Chrome), falling back to pako
* (which is only used to encode in our Node tests).
* @param {string} string
* @param {{gzip: boolean}} options
* @return {Promise<string>}
*/
async function toBase64(string, options) {
let bytes = new TextEncoder().encode(string);

if (options.gzip) {
if (typeof CompressionStream !== 'undefined') {
const cs = new CompressionStream('gzip');
const writer = cs.writable.getWriter();
writer.write(bytes);
writer.close();
const compAb = await new Response(cs.readable).arrayBuffer();
bytes = new Uint8Array(compAb);
} else {
/** @type {import('pako')=} */
const pako = window.pako;
bytes = pako.gzip(string);
}
}

let binaryString = '';
// This is ~25% faster than building the string one character at a time.
// https://jsbench.me/2gkoxazvjl
const chunkSize = 5000;
for (let i = 0; i < bytes.length; i += chunkSize) {
binaryString += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
}
return btoa_(binaryString);
}

/**
* @param {string} encoded
* @param {{gzip: boolean}} options
* @return {string}
*/
function fromBase64(encoded, options) {
const binaryString = atob_(encoded);
const bytes = Uint8Array.from(binaryString, c => c.charCodeAt(0));

if (options.gzip) {
/** @type {import('pako')=} */
const pako = window.pako;
return pako.ungzip(bytes, {to: 'string'});
} else {
return new TextDecoder().decode(bytes);
}
}

if (typeof module !== 'undefined' && module.exports) {
module.exports = {toBase64, fromBase64};
} else {
self.TextEncoding = {toBase64, fromBase64};
}
41 changes: 41 additions & 0 deletions lighthouse-core/test/report/html/renderer/text-encoding-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
'use strict';

const TextEncoding = require('../../../../report/html/renderer/text-encoding.js');

/* eslint-env jest */

describe('TextEncoding', () => {
beforeAll(() => {
global.window = {pako: require('pako')};
});

afterAll(() => {
global.window = undefined;
});

/** @type {string} */
async function test(str) {
for (const gzip of [false, true]) {
const binary = await TextEncoding.toBase64(str, {gzip});
const roundtrip = TextEncoding.fromBase64(binary, {gzip});
expect(roundtrip.length).toEqual(str.length);
expect(roundtrip).toEqual(str);
}
}

it('works', async () => {
await test('');
await test('hello');
await test('😃');
await test('{åß∂œ∑´}');
await test('Some examples of emoji are 😃, 🧘🏻‍♂️, 🌍, 🍞, 🚗, 📞, 🎉, ♥️, 🍆, and 🏁.');
await test('.'.repeat(125183));
await test('😃'.repeat(125183));
await test(JSON.stringify(require('../../../../../lighthouse-treemap/app/debug.json')));
});
});
26 changes: 12 additions & 14 deletions lighthouse-treemap/app/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

/* eslint-env browser */

/* globals I18n webtreemap strings TreemapUtil Tabulator Cell Row DragAndDrop Logger GithubApi */
/* globals I18n webtreemap strings TreemapUtil TextEncoding Tabulator Cell Row DragAndDrop Logger GithubApi */

const DUPLICATED_MODULES_IGNORE_THRESHOLD = 1024;
const DUPLICATED_MODULES_IGNORE_ROOT_RATIO = 0.01;
Expand Down Expand Up @@ -884,22 +884,13 @@ class LighthouseTreemap {
}
}

/**
* @param {string} encoded
*/
function fromBinary(encoded) {
const binary = atob(encoded);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return String.fromCharCode(...new Uint16Array(bytes.buffer));
}

async function main() {
const app = new LighthouseTreemap();
const queryParams = new URLSearchParams(window.location.search);
const hashParams = location.hash ? JSON.parse(fromBinary(location.hash.substr(1))) : {};
const gzip = queryParams.get('gzip') === '1';
const hashParams = location.hash ?
JSON.parse(TextEncoding.fromBase64(location.hash.substr(1), {gzip})) :
{};
/** @type {Record<string, any>} */
const params = {
...Object.fromEntries(queryParams.entries()),
Expand All @@ -912,6 +903,11 @@ async function main() {
} else if ('debug' in params) {
const response = await fetch('debug.json');
app.init(await response.json());
} else if (params.lhr) {
const options = {
lhr: params.lhr,
};
app.init(options);
} else if (params.gist) {
let json;
let options;
Expand All @@ -923,6 +919,7 @@ async function main() {
}
if (options) app.init(options);
} else {
// TODO: remove for v8.
window.addEventListener('message', e => {
if (e.source !== self.opener) return;

Expand All @@ -938,6 +935,7 @@ async function main() {
});
}

// TODO: remove for v8.
// If the page was opened as a popup, tell the opening window we're ready.
if (self.opener && !self.opener.closed) {
self.opener.postMessage({opened: true}, '*');
Expand Down
Loading

0 comments on commit 0689e4e

Please sign in to comment.