Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

report: gzip treemap data #12519

Merged
merged 35 commits into from
May 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d5123ca
report: encode data even if very large
connorjclark May 19, 2021
267d578
ok
connorjclark May 19, 2021
d7281c0
rm
connorjclark May 20, 2021
ff0bfd1
no btoa lol
connorjclark May 20, 2021
c417fcb
benchmark
connorjclark May 20, 2021
8ac3a39
lol
connorjclark May 20, 2021
720c427
test
connorjclark May 20, 2021
f0e8949
pako
connorjclark May 20, 2021
6d6e453
fixtest
connorjclark May 20, 2021
ef3e69e
test
connorjclark May 20, 2021
c9c60df
minify
connorjclark May 20, 2021
585bfa0
Merge remote-tracking branch 'origin/master' into treemap-encode
connorjclark May 20, 2021
a638270
tweak
connorjclark May 20, 2021
9eb78f6
dont use minified
connorjclark May 20, 2021
d985f61
gzip optional
connorjclark May 20, 2021
13a4671
upgrade puppeteer for compression stream
connorjclark May 20, 2021
07e3c8d
revert puppeteer upgrade
connorjclark May 21, 2021
ea226a6
Merge remote-tracking branch 'origin/master' into treemap-encode
connorjclark May 21, 2021
5365e53
update
connorjclark May 21, 2021
a6ff9bb
yay
connorjclark May 21, 2021
b473636
pr
connorjclark May 21, 2021
50ff2f1
names
connorjclark May 24, 2021
ca16fff
deprecate postmessage
connorjclark May 24, 2021
5b2f7d3
rename
connorjclark May 25, 2021
b8e6b84
tweak
connorjclark May 25, 2021
c6dafbd
pr
connorjclark May 25, 2021
56a55e0
rename
connorjclark May 25, 2021
363022e
comment
connorjclark May 25, 2021
e33568a
rename
connorjclark May 25, 2021
941cadb
tweak
connorjclark May 26, 2021
d55f819
Update types/html-renderer.d.ts
connorjclark May 26, 2021
b8d8aff
edit
connorjclark May 27, 2021
f757094
connorjclark May 27, 2021
f0a38c0
Merge remote-tracking branch 'origin/master' into treemap-encode
connorjclark May 27, 2021
d45dc4b
merge
connorjclark May 27, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
*/
Copy link
Member

@brendankenny brendankenny May 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

speaking of #12570 (comment), there used to be a thing with the closure license checker and the files in renderer/, which is why they all have the usual long(er) form of the Apache 2 header. Have you tried importing this yet? Or is that no longer flagged

edit: or I guess it was license-header line length? Weird. But still, same question :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like something that would only show up in the CL check, which I never did (just built and manually tested). If that's the case, I'll edit in the import and just make a new PR here that can land whenever.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like something that would only show up in the CL check, which I never did (just built and manually tested). If that's the case, I'll edit in the import and just make a new PR here that can land whenever.

Is there any downside to changing it now?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

snippet renderer has the same comment and is in BUILD, so it should be fine

'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