@samitouri / QOS-React-2 / commits / 25f09e3e4e

DevTools: Parse named source AST in a worker (#21902)

Resolves #21855 Ended up using workerize in order to setup the worker once it allows easy imports (for babel's parse function) and exports.

Lucas Correia committed Jul 21, 2021 at 13:16 UTC 25f09e3e4e900fc5d510243ce9ed3272d0af59e7
9 files changed +161 -45
packages/react-devtools-extensions/firefox/manifest.json
+1 -1
@@ -32,7 +32,7 @@
32
33 "devtools_page": "main.html",
34
35 - "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
35 + "content_security_policy": "script-src 'self' 'unsafe-eval' blob:; object-src 'self'",
36 "web_accessible_resources": [
37 "main.html",
38 "panel.html",
packages/react-devtools-extensions/package.json
+3 -2
@@ -19,7 +19,6 @@
19 "update-mock-source-maps": "node ./src/__tests__/updateMockSourceMaps.js"
20 },
21 "devDependencies": {
22 - "acorn-jsx": "^5.2.0",
22 "@babel/core": "^7.11.1",
23 "@babel/plugin-proposal-class-properties": "^7.10.4",
24 "@babel/plugin-transform-flow-strip-types": "^7.10.4",
@@ -28,6 +27,7 @@
27 "@babel/preset-env": "^7.11.0",
28 "@babel/preset-flow": "^7.10.4",
29 "@babel/preset-react": "^7.10.4",
30 + "acorn-jsx": "^5.2.0",
31 "archiver": "^3.0.0",
32 "babel-core": "^7.0.0-bridge",
33 "babel-eslint": "^9.0.0",
@@ -55,7 +55,8 @@
55 "web-ext": "^3.0.0",
56 "webpack": "^4.43.0",
57 "webpack-cli": "^3.3.11",
58 - "webpack-dev-server": "^3.10.3"
58 + "webpack-dev-server": "^3.10.3",
59 + "workerize-loader": "^1.3.0"
60 },
61 "dependencies": {
62 "web-ext": "^4"
packages/react-devtools-extensions/src/__tests__/parseHookNames-test.js
+99 -32
@@ -11,6 +11,37 @@
11 // This is done to control if and how the code is transformed at runtime.
12 // Do not declare test components within this test file as it is very fragile.
13
14 +function expectHookNamesToEqual(map, expectedNamesArray) {
15 + // Slightly hacky since it relies on the iterable order of values()
16 + expect(Array.from(map.values())).toEqual(expectedNamesArray);
17 +}
18 +
19 +function requireText(path, encoding) {
20 + const {existsSync, readFileSync} = require('fs');
21 + if (existsSync(path)) {
22 + return Promise.resolve(readFileSync(path, encoding));
23 + } else {
24 + return Promise.reject(`File not found "${path}"`);
25 + }
26 +}
27 +
28 +const chromeGlobal = {
29 + extension: {
30 + getURL: jest.fn((...args) => {
31 + const {join} = require('path');
32 + return join(
33 + __dirname,
34 + '..',
35 + '..',
36 + 'node_modules',
37 + 'source-map',
38 + 'lib',
39 + 'mappings.wasm',
40 + );
41 + }),
42 + },
43 +};
44 +
45 describe('parseHookNames', () => {
46 let fetchMock;
47 let inspectHooks;
@@ -26,6 +57,9 @@ describe('parseHookNames', () => {
57 fetchMock = require('jest-fetch-mock');
58 fetchMock.enableMocks();
59
60 + // Mock out portion of browser API used by parseHookNames to initialize "source-map".
61 + global.chrome = chromeGlobal;
62 +
63 inspectHooks = require('react-debug-tools/src/ReactDebugHooks')
64 .inspectHooks;
65 parseHookNames = require('../parseHookNames').parseHookNames;
@@ -45,44 +79,12 @@ describe('parseHookNames', () => {
79 fetchMock.mockIf(/.+$/, request => {
80 return requireText(request.url, 'utf8');
81 });
48 -
49 - // Mock out portion of browser API used by parseHookNames to initialize "source-map".
50 - global.chrome = {
51 - extension: {
52 - getURL: jest.fn((...args) => {
53 - const {join} = require('path');
54 - return join(
55 - __dirname,
56 - '..',
57 - '..',
58 - 'node_modules',
59 - 'source-map',
60 - 'lib',
61 - 'mappings.wasm',
62 - );
63 - }),
64 - },
65 - };
82 });
83
84 afterEach(() => {
85 fetch.resetMocks();
86 });
87
72 - function expectHookNamesToEqual(map, expectedNamesArray) {
73 - // Slightly hacky since it relies on the iterable order of values()
74 - expect(Array.from(map.values())).toEqual(expectedNamesArray);
75 - }
76 -
77 - function requireText(path, encoding) {
78 - const {existsSync, readFileSync} = require('fs');
79 - if (existsSync(path)) {
80 - return Promise.resolve(readFileSync(path, encoding));
81 - } else {
82 - return Promise.reject(`File not found "${path}"`);
83 - }
84 - }
85 -
88 async function getHookNamesForComponent(Component, props = {}) {
89 const hooksTree = inspectHooks(Component, props, undefined, true);
90 const hookNames = await parseHookNames(hooksTree);
@@ -344,3 +346,68 @@ describe('parseHookNames', () => {
346 });
347 });
348 });
349 +
350 +describe('parseHookNames worker', () => {
351 + let inspectHooks;
352 + let parseHookNames;
353 + let originalParseHookNamesMock;
354 + let workerizedParseHookNamesMock;
355 +
356 + beforeEach(() => {
357 + window.Worker = undefined;
358 +
359 + originalParseHookNamesMock = jest.fn();
360 + workerizedParseHookNamesMock = jest.fn();
361 +
362 + jest.mock('../parseHookNames/parseHookNames.js', () => {
363 + return {
364 + __esModule: true,
365 + parseHookNames: originalParseHookNamesMock,
366 + };
367 + });
368 +
369 + jest.mock('../parseHookNames/parseHookNames.worker.js', () => {
370 + return {
371 + __esModule: true,
372 + default: () => ({
373 + parseHookNames: workerizedParseHookNamesMock,
374 + }),
375 + };
376 + });
377 +
378 + // Mock out portion of browser API used by parseHookNames to initialize "source-map".
379 + global.chrome = chromeGlobal;
380 +
381 + inspectHooks = require('react-debug-tools/src/ReactDebugHooks')
382 + .inspectHooks;
383 + parseHookNames = require('../parseHookNames').parseHookNames;
384 + });
385 +
386 + async function getHookNamesForComponent(Component, props = {}) {
387 + const hooksTree = inspectHooks(Component, props, undefined, true);
388 + const hookNames = await parseHookNames(hooksTree);
389 + return hookNames;
390 + }
391 +
392 + it('should use worker when available', async () => {
393 + const Component = require('./__source__/__untransformed__/ComponentWithUseState')
394 + .Component;
395 +
396 + window.Worker = true;
397 + // resets module so mocked worker instance can be updated
398 + jest.resetModules();
399 + parseHookNames = require('../parseHookNames').parseHookNames;
400 +
401 + await getHookNamesForComponent(Component);
402 + expect(workerizedParseHookNamesMock).toHaveBeenCalledTimes(1);
403 + });
404 +
405 + it('should use main thread when worker is not available', async () => {
406 + const Component = require('./__source__/__untransformed__/ComponentWithUseState')
407 + .Component;
408 +
409 + await getHookNamesForComponent(Component);
410 + expect(workerizedParseHookNamesMock).toHaveBeenCalledTimes(0);
411 + expect(originalParseHookNamesMock).toHaveBeenCalledTimes(1);
412 + });
413 +});
packages/react-devtools-extensions/src/parseHookNames/index.js new
+33
@@ -0,0 +1,33 @@
1 +/* global chrome */
2 +
3 +/**
4 + * Copyright (c) Facebook, Inc. and its affiliates.
5 + *
6 + * This source code is licensed under the MIT license found in the
7 + * LICENSE file in the root directory of this source tree.
8 + *
9 + * @flow
10 + */
11 +
12 +// This file uses workerize to load ./parseHookNames.worker as a webworker
13 +// and instanciates it, exposing flow typed functions that can be used
14 +// on other files.
15 +
16 +import * as parseHookNamesModule from './parseHookNames';
17 +import WorkerizedParseHookNames from './parseHookNames.worker';
18 +
19 +type ParseHookNamesModule = typeof parseHookNamesModule;
20 +
21 +// $FlowFixMe
22 +const wasmMappingsURL = chrome.extension.getURL('mappings.wasm');
23 +
24 +const workerizedParseHookNames: ParseHookNamesModule = window.Worker
25 + ? WorkerizedParseHookNames()
26 + : parseHookNamesModule;
27 +
28 +type ParseHookNames = $PropertyType<ParseHookNamesModule, 'parseHookNames'>;
29 +
30 +export const parseHookNames: ParseHookNames = hooksTree =>
31 + workerizedParseHookNames.parseHookNames(hooksTree, wasmMappingsURL);
32 +
33 +export const purgeCachedMetadata = workerizedParseHookNames.purgeCachedMetadata;
packages/react-devtools-extensions/src/parseHookNames/parseHookNames.js renamed
+8 -9
@@ -1,5 +1,3 @@
1 -/* global chrome */
2 -
1 /**
2 * Copyright (c) Facebook, Inc. and its affiliates.
3 *
@@ -12,8 +10,8 @@
10 import {parse} from '@babel/parser';
11 import LRU from 'lru-cache';
12 import {SourceMapConsumer} from 'source-map';
15 -import {getHookName} from './astUtils';
16 -import {areSourceMapsAppliedToErrors} from './ErrorTester';
13 +import {getHookName} from '../astUtils';
14 +import {areSourceMapsAppliedToErrors} from '../ErrorTester';
15 import {__DEBUG__} from 'react-devtools-shared/src/constants';
16 import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache';
17
@@ -24,7 +22,7 @@ import type {
22 } from 'react-debug-tools/src/ReactDebugHooks';
23 import type {HookNames, LRUCache} from 'react-devtools-shared/src/types';
24 import type {Thenable} from 'shared/ReactTypes';
27 -import type {SourceConsumer} from './astUtils';
25 +import type {SourceConsumer} from '../astUtils';
26
27 const SOURCE_MAP_REGEX = / ?sourceMappingURL=([^\s'"]+)/gm;
28 const MAX_SOURCE_LENGTH = 100_000_000;
@@ -103,6 +101,7 @@ const originalURLToMetadataCache: LRUCache<
101
102 export async function parseHookNames(
103 hooksTree: HooksTree,
104 + wasmMappingsURL: string,
105 ): Thenable<HookNames | null> {
106 const hooksList: Array<HooksNode> = [];
107 flattenHooksList(hooksTree, hooksList);
@@ -160,7 +159,9 @@ export async function parseHookNames(
159 }
160
161 return loadSourceFiles(locationKeyToHookSourceData)
163 - .then(() => extractAndLoadSourceMaps(locationKeyToHookSourceData))
162 + .then(() =>
163 + extractAndLoadSourceMaps(locationKeyToHookSourceData, wasmMappingsURL),
164 + )
165 .then(() => parseSourceAST(locationKeyToHookSourceData))
166 .then(() => updateLruCache(locationKeyToHookSourceData))
167 .then(() => findHookNames(hooksList, locationKeyToHookSourceData));
@@ -182,6 +183,7 @@ function decodeBase64String(encoded: string): Object {
183
184 function extractAndLoadSourceMaps(
185 locationKeyToHookSourceData: Map<string, HookSourceData>,
186 + wasmMappingsURL: string,
187 ): Promise<*> {
188 // SourceMapConsumer.initialize() does nothing when running in Node (aka Jest)
189 // because the wasm file is automatically read from the file system
@@ -193,9 +195,6 @@ function extractAndLoadSourceMaps(
195 );
196 }
197
196 - // $FlowFixMe
197 - const wasmMappingsURL = chrome.extension.getURL('mappings.wasm');
198 -
198 SourceMapConsumer.initialize({'lib/mappings.wasm': wasmMappingsURL});
199 }
200
packages/react-devtools-extensions/src/parseHookNames/parseHookNames.worker.js new
+4
@@ -0,0 +1,4 @@
1 +import * as parseHookNamesModule from './parseHookNames';
2 +
3 +export const parseHookNames = parseHookNamesModule.parseHookNames;
4 +export const purgeCachedMetadata = parseHookNamesModule.purgeCachedMetadata;
packages/react-devtools-extensions/webpack.config.js
+5
@@ -109,6 +109,11 @@ module.exports = {
109 },
110 ],
111 },
112 + {
113 + test: /\.worker\.js$/,
114 + // inline: true due to limitations with extensions
115 + use: {loader: 'workerize-loader', options: {inline: true}},
116 + },
117 ],
118 },
119 };
packages/react-devtools-shared/src/hookNamesCache.js
+1 -1
@@ -18,7 +18,7 @@ import type {
18 } from 'react-devtools-shared/src/types';
19 import type {HookSource} from 'react-debug-tools/src/ReactDebugHooks';
20
21 -const TIMEOUT = 5000;
21 +const TIMEOUT = 30000;
22
23 const Pending = 0;
24 const Resolved = 1;
yarn.lock
+7
@@ -14877,6 +14877,13 @@ worker-loader@^3.0.2:
14877 loader-utils "^2.0.0"
14878 schema-utils "^2.7.0"
14879
14880 +workerize-loader@^1.3.0:
14881 + version "1.3.0"
14882 + resolved "https://registry.yarnpkg.com/workerize-loader/-/workerize-loader-1.3.0.tgz#4995cf2ff2b45dd6dc60e4411e63f5ae2c704d36"
14883 + integrity sha512-utWDc8K6embcICmRBUUkzanPgKBb8yM1OHfh6siZfiMsswE8wLCa9CWS+L7AARz0+Th4KH4ZySrqer/OJ9WuWw==
14884 + dependencies:
14885 + loader-utils "^2.0.0"
14886 +
14887 wrap-ansi@^2.0.0:
14888 version "2.1.0"
14889 resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"