mirror of https://github.com/status-im/metro.git
packager: add GlobalTransformCache
Reviewed By: davidaurelio Differential Revision: D4175938 fbshipit-source-id: 1f57d594b4c8c8189feb2ea6d4d4011870ffd85f
This commit is contained in:
parent
a810087624
commit
158da01d26
|
@ -19,7 +19,8 @@ jest.setMock('worker-farm', function() { return () => {}; })
|
||||||
.mock('../../AssetServer')
|
.mock('../../AssetServer')
|
||||||
.mock('../../lib/declareOpts')
|
.mock('../../lib/declareOpts')
|
||||||
.mock('../../node-haste')
|
.mock('../../node-haste')
|
||||||
.mock('../../Logger');
|
.mock('../../Logger')
|
||||||
|
.mock('../../lib/GlobalTransformCache');
|
||||||
|
|
||||||
describe('processRequest', () => {
|
describe('processRequest', () => {
|
||||||
let SourceMapConsumer, Bundler, Server, AssetServer, Promise;
|
let SourceMapConsumer, Bundler, Server, AssetServer, Promise;
|
||||||
|
|
|
@ -0,0 +1,198 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2016-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*
|
||||||
|
* @flow
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const debounce = require('lodash/debounce');
|
||||||
|
const imurmurhash = require('imurmurhash');
|
||||||
|
const jsonStableStringify = require('json-stable-stringify');
|
||||||
|
const path = require('path');
|
||||||
|
const request = require('request');
|
||||||
|
const toFixedHex = require('./toFixedHex');
|
||||||
|
|
||||||
|
import type {CachedResult} from './TransformCache';
|
||||||
|
|
||||||
|
const SINGLE_REQUEST_MAX_KEYS = 100;
|
||||||
|
const AGGREGATION_DELAY_MS = 100;
|
||||||
|
|
||||||
|
type FetchResultURIs = (
|
||||||
|
keys: Array<string>,
|
||||||
|
callback: (error?: Error, results?: Map<string, string>) => void,
|
||||||
|
) => mixed;
|
||||||
|
|
||||||
|
type FetchProps = {
|
||||||
|
filePath: string,
|
||||||
|
sourceCode: string,
|
||||||
|
transformCacheKey: string,
|
||||||
|
transformOptions: mixed,
|
||||||
|
};
|
||||||
|
|
||||||
|
type FetchCallback = (error?: Error, resultURI?: ?CachedResult) => mixed;
|
||||||
|
type FetchURICallback = (error?: Error, resultURI?: ?string) => mixed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We aggregate the requests to do a single request for many keys. It also
|
||||||
|
* ensures we do a single request at a time to avoid pressuring the I/O.
|
||||||
|
*/
|
||||||
|
class KeyURIFetcher {
|
||||||
|
|
||||||
|
_fetchResultURIs: FetchResultURIs;
|
||||||
|
_pendingQueries: Array<{key: string, callback: FetchURICallback}>;
|
||||||
|
_isProcessing: boolean;
|
||||||
|
_processQueriesDebounced: () => void;
|
||||||
|
_processQueries: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the pending keys right now, if any and if we're not already doing
|
||||||
|
* so in parallel. At the end of the fetch, we trigger a new batch fetching
|
||||||
|
* recursively.
|
||||||
|
*/
|
||||||
|
_processQueries() {
|
||||||
|
const {_pendingQueries} = this;
|
||||||
|
if (_pendingQueries.length === 0 || this._isProcessing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._isProcessing = true;
|
||||||
|
const queries = _pendingQueries.splice(0, SINGLE_REQUEST_MAX_KEYS);
|
||||||
|
const keys = queries.map(query => query.key);
|
||||||
|
this._fetchResultURIs(keys, (error, results) => {
|
||||||
|
queries.forEach(query => {
|
||||||
|
query.callback(error, results && results.get(query.key));
|
||||||
|
});
|
||||||
|
this._isProcessing = false;
|
||||||
|
process.nextTick(this._processQueries);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue the fetching of a particular key.
|
||||||
|
*/
|
||||||
|
fetch(key: string, callback: FetchURICallback) {
|
||||||
|
this._pendingQueries.push({key, callback});
|
||||||
|
this._processQueriesDebounced();
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(fetchResultURIs: FetchResultURIs) {
|
||||||
|
this._fetchResultURIs = fetchResultURIs;
|
||||||
|
this._pendingQueries = [];
|
||||||
|
this._isProcessing = false;
|
||||||
|
this._processQueries = this._processQueries.bind(this);
|
||||||
|
this._processQueriesDebounced =
|
||||||
|
debounce(this._processQueries, AGGREGATION_DELAY_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateCachedResult(cachedResult: mixed): ?CachedResult {
|
||||||
|
if (
|
||||||
|
cachedResult != null &&
|
||||||
|
typeof cachedResult === 'object' &&
|
||||||
|
typeof cachedResult.code === 'string' &&
|
||||||
|
Array.isArray(cachedResult.dependencies) &&
|
||||||
|
cachedResult.dependencies.every(dep => typeof dep === 'string') &&
|
||||||
|
Array.isArray(cachedResult.dependencyOffsets) &&
|
||||||
|
cachedResult.dependencyOffsets.every(offset => typeof offset === 'number')
|
||||||
|
) {
|
||||||
|
return (cachedResult: any);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One can enable the global cache by calling configure() from a custom CLI
|
||||||
|
* script. Eventually we may make it more flexible.
|
||||||
|
*/
|
||||||
|
class GlobalTransformCache {
|
||||||
|
|
||||||
|
_fetcher: KeyURIFetcher;
|
||||||
|
static _global: ?GlobalTransformCache;
|
||||||
|
|
||||||
|
constructor(fetchResultURIs: FetchResultURIs) {
|
||||||
|
this._fetcher = new KeyURIFetcher(fetchResultURIs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a key for identifying uniquely a source file.
|
||||||
|
*/
|
||||||
|
static keyOf(props: FetchProps) {
|
||||||
|
const sourceDigest = toFixedHex(8, imurmurhash(props.sourceCode).result());
|
||||||
|
const optionsHash = imurmurhash()
|
||||||
|
.hash(jsonStableStringify(props.transformOptions) || '')
|
||||||
|
.hash(props.transformCacheKey)
|
||||||
|
.result();
|
||||||
|
const optionsDigest = toFixedHex(8, optionsHash);
|
||||||
|
return (
|
||||||
|
`${optionsDigest}${sourceDigest}` +
|
||||||
|
`${path.basename(props.filePath)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We may want to improve that logic to return a stream instead of the whole
|
||||||
|
* blob of transformed results. However the results are generally only a few
|
||||||
|
* megabytes each.
|
||||||
|
*/
|
||||||
|
_fetchFromURI(uri: string, callback: FetchCallback) {
|
||||||
|
request.get({uri, json: true}, (error, response, unvalidatedResult) => {
|
||||||
|
if (error != null) {
|
||||||
|
callback(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (response.statusCode !== 200) {
|
||||||
|
callback(new Error(
|
||||||
|
`Unexpected HTTP status code: ${response.statusCode}`,
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = validateCachedResult(unvalidatedResult);
|
||||||
|
if (result == null) {
|
||||||
|
callback(new Error('Invalid result returned by server.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback(undefined, result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(props: FetchProps, callback: FetchCallback) {
|
||||||
|
this._fetcher.fetch(GlobalTransformCache.keyOf(props), (error, uri) => {
|
||||||
|
if (error != null) {
|
||||||
|
callback(error);
|
||||||
|
} else {
|
||||||
|
if (uri == null) {
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._fetchFromURI(uri, callback);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For using the global cache one needs to have some kind of central key-value
|
||||||
|
* store that gets prefilled using keyOf() and the transformed results. The
|
||||||
|
* fetching function should provide a mapping of keys to URIs. The files
|
||||||
|
* referred by these URIs contains the transform results. Using URIs instead
|
||||||
|
* of returning the content directly allows for independent fetching of each
|
||||||
|
* result.
|
||||||
|
*/
|
||||||
|
static configure(fetchResultURIs: FetchResultURIs) {
|
||||||
|
GlobalTransformCache._global = new GlobalTransformCache(fetchResultURIs);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get() {
|
||||||
|
return GlobalTransformCache._global;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalTransformCache._global = null;
|
||||||
|
|
||||||
|
module.exports = GlobalTransformCache;
|
|
@ -22,6 +22,7 @@ const jsonStableStringify = require('json-stable-stringify');
|
||||||
const mkdirp = require('mkdirp');
|
const mkdirp = require('mkdirp');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const rimraf = require('rimraf');
|
const rimraf = require('rimraf');
|
||||||
|
const toFixedHex = require('./toFixedHex');
|
||||||
const writeFileAtomicSync = require('write-file-atomic').sync;
|
const writeFileAtomicSync = require('write-file-atomic').sync;
|
||||||
|
|
||||||
const CACHE_NAME = 'react-native-packager-cache';
|
const CACHE_NAME = 'react-native-packager-cache';
|
||||||
|
@ -66,15 +67,14 @@ function getCacheFilePaths(props: {
|
||||||
const hasher = imurmurhash()
|
const hasher = imurmurhash()
|
||||||
.hash(props.filePath)
|
.hash(props.filePath)
|
||||||
.hash(jsonStableStringify(props.transformOptions) || '');
|
.hash(jsonStableStringify(props.transformOptions) || '');
|
||||||
let hash = hasher.result().toString(16);
|
const hash = toFixedHex(8, hasher.result());
|
||||||
hash = Array(8 - hash.length + 1).join('0') + hash;
|
|
||||||
const prefix = hash.substr(0, 2);
|
const prefix = hash.substr(0, 2);
|
||||||
const fileName = `${hash.substr(2)}${path.basename(props.filePath)}`;
|
const fileName = `${hash.substr(2)}${path.basename(props.filePath)}`;
|
||||||
const base = path.join(getCacheDirPath(), prefix, fileName);
|
const base = path.join(getCacheDirPath(), prefix, fileName);
|
||||||
return {transformedCode: base, metadata: base + '.meta'};
|
return {transformedCode: base, metadata: base + '.meta'};
|
||||||
}
|
}
|
||||||
|
|
||||||
type CachedResult = {
|
export type CachedResult = {
|
||||||
code: string,
|
code: string,
|
||||||
dependencies: Array<string>,
|
dependencies: Array<string>,
|
||||||
dependencyOffsets: Array<number>,
|
dependencyOffsets: Array<number>,
|
||||||
|
@ -135,7 +135,7 @@ function writeSync(props: {
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
type CacheOptions = {resetCache?: boolean};
|
export type CacheOptions = {resetCache?: boolean};
|
||||||
|
|
||||||
/* 1 day */
|
/* 1 day */
|
||||||
const GARBAGE_COLLECTION_PERIOD = 24 * 60 * 60 * 1000;
|
const GARBAGE_COLLECTION_PERIOD = 24 * 60 * 60 * 1000;
|
||||||
|
@ -272,6 +272,14 @@ function readMetadataFileSync(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ReadTransformProps = {
|
||||||
|
filePath: string,
|
||||||
|
sourceCode: string,
|
||||||
|
transformOptions: mixed,
|
||||||
|
transformCacheKey: string,
|
||||||
|
cacheOptions: CacheOptions,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We verify the source hash matches to ensure we always favor rebuilding when
|
* We verify the source hash matches to ensure we always favor rebuilding when
|
||||||
* source change (rather than just using fs.mtime(), a bit less robust).
|
* source change (rather than just using fs.mtime(), a bit less robust).
|
||||||
|
@ -285,13 +293,7 @@ function readMetadataFileSync(
|
||||||
* Meanwhile we store transforms with different options in different files so
|
* Meanwhile we store transforms with different options in different files so
|
||||||
* that it is fast to switch between ex. minified, or not.
|
* that it is fast to switch between ex. minified, or not.
|
||||||
*/
|
*/
|
||||||
function readSync(props: {
|
function readSync(props: ReadTransformProps): ?CachedResult {
|
||||||
filePath: string,
|
|
||||||
sourceCode: string,
|
|
||||||
transformOptions: mixed,
|
|
||||||
transformCacheKey: string,
|
|
||||||
cacheOptions: CacheOptions,
|
|
||||||
}): ?CachedResult {
|
|
||||||
GARBAGE_COLLECTOR.collectIfNecessarySync(props.cacheOptions);
|
GARBAGE_COLLECTOR.collectIfNecessarySync(props.cacheOptions);
|
||||||
const cacheFilePaths = getCacheFilePaths(props);
|
const cacheFilePaths = getCacheFilePaths(props);
|
||||||
let metadata, transformedCode;
|
let metadata, transformedCode;
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2016-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*
|
||||||
|
* @flow
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function get() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {get};
|
|
@ -12,7 +12,9 @@
|
||||||
jest
|
jest
|
||||||
.dontMock('imurmurhash')
|
.dontMock('imurmurhash')
|
||||||
.dontMock('json-stable-stringify')
|
.dontMock('json-stable-stringify')
|
||||||
.dontMock('../TransformCache');
|
.dontMock('../TransformCache')
|
||||||
|
.dontMock('../toFixedHex')
|
||||||
|
.dontMock('left-pad');
|
||||||
|
|
||||||
const imurmurhash = require('imurmurhash');
|
const imurmurhash = require('imurmurhash');
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2016-present, Facebook, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the BSD-style license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree. An additional grant
|
||||||
|
* of patent rights can be found in the PATENTS file in the same directory.
|
||||||
|
*
|
||||||
|
* @flow
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const leftPad = require('left-pad');
|
||||||
|
|
||||||
|
function toFixedHex(length: number, number: number): string {
|
||||||
|
return leftPad(number.toString(16), length, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = toFixedHex;
|
|
@ -11,8 +11,10 @@
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const GlobalTransformCache = require('../lib/GlobalTransformCache');
|
||||||
const TransformCache = require('../lib/TransformCache');
|
const TransformCache = require('../lib/TransformCache');
|
||||||
|
|
||||||
|
const chalk = require('chalk');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const docblock = require('./DependencyGraph/docblock');
|
const docblock = require('./DependencyGraph/docblock');
|
||||||
const invariant = require('invariant');
|
const invariant = require('invariant');
|
||||||
|
@ -22,6 +24,7 @@ const jsonStableStringify = require('json-stable-stringify');
|
||||||
const {join: joinPath, relative: relativePath, extname} = require('path');
|
const {join: joinPath, relative: relativePath, extname} = require('path');
|
||||||
|
|
||||||
import type {TransformedCode} from '../JSTransformer/worker/worker';
|
import type {TransformedCode} from '../JSTransformer/worker/worker';
|
||||||
|
import type {ReadTransformProps} from '../lib/TransformCache';
|
||||||
import type Cache from './Cache';
|
import type Cache from './Cache';
|
||||||
import type DependencyGraphHelpers from './DependencyGraph/DependencyGraphHelpers';
|
import type DependencyGraphHelpers from './DependencyGraph/DependencyGraphHelpers';
|
||||||
import type ModuleCache from './ModuleCache';
|
import type ModuleCache from './ModuleCache';
|
||||||
|
@ -73,6 +76,8 @@ class Module {
|
||||||
_readSourceCodePromise: Promise<string>;
|
_readSourceCodePromise: Promise<string>;
|
||||||
_readPromises: Map<string, Promise<ReadResult>>;
|
_readPromises: Map<string, Promise<ReadResult>>;
|
||||||
|
|
||||||
|
static _useGlobalCache: boolean;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
file,
|
file,
|
||||||
fastfs,
|
fastfs,
|
||||||
|
@ -214,31 +219,61 @@ class Module {
|
||||||
return {...result, id, source};
|
return {...result, id, source};
|
||||||
}
|
}
|
||||||
|
|
||||||
_transformAndCache(
|
_transformCodeForCallback(
|
||||||
transformOptions: mixed,
|
cacheProps: ReadTransformProps,
|
||||||
callback: (error: ?Error, result: ?TransformedCode) => void,
|
callback: (error: ?Error, result: ?TransformedCode) => void,
|
||||||
) {
|
) {
|
||||||
const {_transformCode, _transformCacheKey} = this;
|
const {_transformCode} = this;
|
||||||
invariant(_transformCode != null, 'missing code transform funtion');
|
invariant(_transformCode != null, 'missing code transform funtion');
|
||||||
invariant(_transformCacheKey != null, 'missing cache key');
|
const {sourceCode, transformOptions} = cacheProps;
|
||||||
this._readSourceCode()
|
return _transformCode(this, sourceCode, transformOptions).then(
|
||||||
.then(sourceCode =>
|
freshResult => process.nextTick(callback, undefined, freshResult),
|
||||||
_transformCode(this, sourceCode, transformOptions)
|
error => process.nextTick(callback, error),
|
||||||
.then(freshResult => {
|
);
|
||||||
TransformCache.writeSync({
|
}
|
||||||
filePath: this.path,
|
|
||||||
sourceCode,
|
_getTransformedCode(
|
||||||
transformCacheKey: _transformCacheKey,
|
cacheProps: ReadTransformProps,
|
||||||
transformOptions,
|
callback: (error: ?Error, result: ?TransformedCode) => void,
|
||||||
result: freshResult,
|
) {
|
||||||
});
|
const globalCache = GlobalTransformCache.get();
|
||||||
return freshResult;
|
if (!Module._useGlobalCache || globalCache == null) {
|
||||||
})
|
this._transformCodeForCallback(cacheProps, callback);
|
||||||
)
|
return;
|
||||||
.then(
|
}
|
||||||
freshResult => process.nextTick(callback, null, freshResult),
|
globalCache.fetch(cacheProps, (globalCacheError, globalCachedResult) => {
|
||||||
error => process.nextTick(callback, error),
|
if (globalCacheError != null && Module._useGlobalCache) {
|
||||||
);
|
console.log(chalk.red(
|
||||||
|
'\nWarning: the global cache failed with error:',
|
||||||
|
));
|
||||||
|
console.log(chalk.red(globalCacheError.stack));
|
||||||
|
console.log(chalk.red(
|
||||||
|
'The global cache will be DISABLED for the ' +
|
||||||
|
'remainder of the transformation.',
|
||||||
|
));
|
||||||
|
Module._useGlobalCache = false;
|
||||||
|
}
|
||||||
|
if (globalCacheError != null || globalCachedResult == null) {
|
||||||
|
this._transformCodeForCallback(cacheProps, callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback(undefined, globalCachedResult);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_getAndCacheTransformedCode(
|
||||||
|
cacheProps: ReadTransformProps,
|
||||||
|
callback: (error: ?Error, result: ?TransformedCode) => void,
|
||||||
|
) {
|
||||||
|
this._getTransformedCode(cacheProps, (error, result) => {
|
||||||
|
if (error) {
|
||||||
|
callback(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
invariant(result != null, 'missing result');
|
||||||
|
TransformCache.writeSync({...cacheProps, result});
|
||||||
|
callback(undefined, result);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -264,20 +299,20 @@ class Module {
|
||||||
}
|
}
|
||||||
const transformCacheKey = this._transformCacheKey;
|
const transformCacheKey = this._transformCacheKey;
|
||||||
invariant(transformCacheKey != null, 'missing transform cache key');
|
invariant(transformCacheKey != null, 'missing transform cache key');
|
||||||
const cachedResult =
|
const cacheProps = {
|
||||||
TransformCache.readSync({
|
filePath: this.path,
|
||||||
filePath: this.path,
|
sourceCode,
|
||||||
sourceCode,
|
transformCacheKey,
|
||||||
transformCacheKey,
|
transformOptions,
|
||||||
transformOptions,
|
cacheOptions: this._options,
|
||||||
cacheOptions: this._options,
|
};
|
||||||
});
|
const cachedResult = TransformCache.readSync(cacheProps);
|
||||||
if (cachedResult) {
|
if (cachedResult) {
|
||||||
return this._finalizeReadResult(sourceCode, id, extern, cachedResult);
|
return Promise.resolve(this._finalizeReadResult(sourceCode, id, extern, cachedResult));
|
||||||
}
|
}
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this._transformAndCache(
|
this._getAndCacheTransformedCode(
|
||||||
transformOptions,
|
cacheProps,
|
||||||
(transformError, freshResult) => {
|
(transformError, freshResult) => {
|
||||||
if (transformError) {
|
if (transformError) {
|
||||||
reject(transformError);
|
reject(transformError);
|
||||||
|
@ -320,6 +355,8 @@ class Module {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Module._useGlobalCache = true;
|
||||||
|
|
||||||
// use weak map to speed up hash creation of known objects
|
// use weak map to speed up hash creation of known objects
|
||||||
const knownHashes = new WeakMap();
|
const knownHashes = new WeakMap();
|
||||||
function stableObjectHash(object) {
|
function stableObjectHash(object) {
|
||||||
|
|
Loading…
Reference in New Issue