Skip to content

Commit 25f09e3

Browse files
authored
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.
1 parent 9ab90de commit 25f09e3

File tree

9 files changed

+161
-45
lines changed

9 files changed

+161
-45
lines changed

packages/react-devtools-extensions/firefox/manifest.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232

3333
"devtools_page": "main.html",
3434

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'",
3636
"web_accessible_resources": [
3737
"main.html",
3838
"panel.html",

packages/react-devtools-extensions/package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
"update-mock-source-maps": "node ./src/__tests__/updateMockSourceMaps.js"
2020
},
2121
"devDependencies": {
22-
"acorn-jsx": "^5.2.0",
2322
"@babel/core": "^7.11.1",
2423
"@babel/plugin-proposal-class-properties": "^7.10.4",
2524
"@babel/plugin-transform-flow-strip-types": "^7.10.4",
@@ -28,6 +27,7 @@
2827
"@babel/preset-env": "^7.11.0",
2928
"@babel/preset-flow": "^7.10.4",
3029
"@babel/preset-react": "^7.10.4",
30+
"acorn-jsx": "^5.2.0",
3131
"archiver": "^3.0.0",
3232
"babel-core": "^7.0.0-bridge",
3333
"babel-eslint": "^9.0.0",
@@ -55,7 +55,8 @@
5555
"web-ext": "^3.0.0",
5656
"webpack": "^4.43.0",
5757
"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"
5960
},
6061
"dependencies": {
6162
"web-ext": "^4"

packages/react-devtools-extensions/src/__tests__/parseHookNames-test.js

+99-32
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,37 @@
1111
// This is done to control if and how the code is transformed at runtime.
1212
// Do not declare test components within this test file as it is very fragile.
1313

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+
1445
describe('parseHookNames', () => {
1546
let fetchMock;
1647
let inspectHooks;
@@ -26,6 +57,9 @@ describe('parseHookNames', () => {
2657
fetchMock = require('jest-fetch-mock');
2758
fetchMock.enableMocks();
2859

60+
// Mock out portion of browser API used by parseHookNames to initialize "source-map".
61+
global.chrome = chromeGlobal;
62+
2963
inspectHooks = require('react-debug-tools/src/ReactDebugHooks')
3064
.inspectHooks;
3165
parseHookNames = require('../parseHookNames').parseHookNames;
@@ -45,44 +79,12 @@ describe('parseHookNames', () => {
4579
fetchMock.mockIf(/.+$/, request => {
4680
return requireText(request.url, 'utf8');
4781
});
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-
};
6682
});
6783

6884
afterEach(() => {
6985
fetch.resetMocks();
7086
});
7187

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-
8688
async function getHookNamesForComponent(Component, props = {}) {
8789
const hooksTree = inspectHooks(Component, props, undefined, true);
8890
const hookNames = await parseHookNames(hooksTree);
@@ -344,3 +346,68 @@ describe('parseHookNames', () => {
344346
});
345347
});
346348
});
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+
});
Original file line numberDiff line numberDiff line change
@@ -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.js renamed to packages/react-devtools-extensions/src/parseHookNames/parseHookNames.js

+8-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
/* global chrome */
2-
31
/**
42
* Copyright (c) Facebook, Inc. and its affiliates.
53
*
@@ -12,8 +10,8 @@
1210
import {parse} from '@babel/parser';
1311
import LRU from 'lru-cache';
1412
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';
1715
import {__DEBUG__} from 'react-devtools-shared/src/constants';
1816
import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache';
1917

@@ -24,7 +22,7 @@ import type {
2422
} from 'react-debug-tools/src/ReactDebugHooks';
2523
import type {HookNames, LRUCache} from 'react-devtools-shared/src/types';
2624
import type {Thenable} from 'shared/ReactTypes';
27-
import type {SourceConsumer} from './astUtils';
25+
import type {SourceConsumer} from '../astUtils';
2826

2927
const SOURCE_MAP_REGEX = / ?sourceMappingURL=([^\s'"]+)/gm;
3028
const MAX_SOURCE_LENGTH = 100_000_000;
@@ -103,6 +101,7 @@ const originalURLToMetadataCache: LRUCache<
103101

104102
export async function parseHookNames(
105103
hooksTree: HooksTree,
104+
wasmMappingsURL: string,
106105
): Thenable<HookNames | null> {
107106
const hooksList: Array<HooksNode> = [];
108107
flattenHooksList(hooksTree, hooksList);
@@ -160,7 +159,9 @@ export async function parseHookNames(
160159
}
161160

162161
return loadSourceFiles(locationKeyToHookSourceData)
163-
.then(() => extractAndLoadSourceMaps(locationKeyToHookSourceData))
162+
.then(() =>
163+
extractAndLoadSourceMaps(locationKeyToHookSourceData, wasmMappingsURL),
164+
)
164165
.then(() => parseSourceAST(locationKeyToHookSourceData))
165166
.then(() => updateLruCache(locationKeyToHookSourceData))
166167
.then(() => findHookNames(hooksList, locationKeyToHookSourceData));
@@ -182,6 +183,7 @@ function decodeBase64String(encoded: string): Object {
182183

183184
function extractAndLoadSourceMaps(
184185
locationKeyToHookSourceData: Map<string, HookSourceData>,
186+
wasmMappingsURL: string,
185187
): Promise<*> {
186188
// SourceMapConsumer.initialize() does nothing when running in Node (aka Jest)
187189
// because the wasm file is automatically read from the file system
@@ -193,9 +195,6 @@ function extractAndLoadSourceMaps(
193195
);
194196
}
195197

196-
// $FlowFixMe
197-
const wasmMappingsURL = chrome.extension.getURL('mappings.wasm');
198-
199198
SourceMapConsumer.initialize({'lib/mappings.wasm': wasmMappingsURL});
200199
}
201200

Original file line numberDiff line numberDiff line change
@@ -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
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ module.exports = {
109109
},
110110
],
111111
},
112+
{
113+
test: /\.worker\.js$/,
114+
// inline: true due to limitations with extensions
115+
use: {loader: 'workerize-loader', options: {inline: true}},
116+
},
112117
],
113118
},
114119
};

packages/react-devtools-shared/src/hookNamesCache.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import type {
1818
} from 'react-devtools-shared/src/types';
1919
import type {HookSource} from 'react-debug-tools/src/ReactDebugHooks';
2020

21-
const TIMEOUT = 5000;
21+
const TIMEOUT = 30000;
2222

2323
const Pending = 0;
2424
const Resolved = 1;

yarn.lock

+7
Original file line numberDiff line numberDiff line change
@@ -14877,6 +14877,13 @@ worker-loader@^3.0.2:
1487714877
loader-utils "^2.0.0"
1487814878
schema-utils "^2.7.0"
1487914879

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+
1488014887
wrap-ansi@^2.0.0:
1488114888
version "2.1.0"
1488214889
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"

0 commit comments

Comments
 (0)