feat: small performance wins
- Delay initializing unimportant parts until index module (ID 0) is required - Lower memory usage by interacting with Metro's internal module list directly
This commit is contained in:
parent
0bea1e29be
commit
e9fedc8db4
11 changed files with 215 additions and 166 deletions
|
|
@ -2,9 +2,9 @@ import { getCurrentStack } from '@revenge-mod/utils/errors'
|
|||
import { proxify } from '@revenge-mod/utils/proxy'
|
||||
import { cache } from '../caches'
|
||||
import {
|
||||
mDeps,
|
||||
mImportedPaths,
|
||||
mInitialized,
|
||||
mMetadata,
|
||||
mUninitialized,
|
||||
} from '../metro/_internal'
|
||||
import {
|
||||
|
|
@ -184,7 +184,7 @@ export function* lookupModules(filter: Filter, options?: LookupModulesOptions) {
|
|||
|
||||
// Full lookup
|
||||
if (options?.all) {
|
||||
for (const id of mMetadata.keys()) {
|
||||
for (let id = 0; id < mDeps.length; id++) {
|
||||
if (cached?.has(id)) continue
|
||||
|
||||
const exports = getInitializedModuleExports(id)
|
||||
|
|
@ -294,7 +294,7 @@ export function lookupModule(filter: Filter, options?: LookupModulesOptions) {
|
|||
|
||||
// Full lookup
|
||||
if (options?.all) {
|
||||
for (const id of mMetadata.keys()) {
|
||||
for (let id = 0; id < mDeps.length; id++) {
|
||||
const exports = getInitializedModuleExports(id)
|
||||
const flag = runFilter(filter, id, exports, options)
|
||||
if (flag)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import {
|
|||
executeInitializeSubscriptions,
|
||||
executeRequireSubscriptions,
|
||||
} from './subscriptions/_internal'
|
||||
import { isModuleExportsBad } from './utils'
|
||||
import type { Metro } from '../types'
|
||||
|
||||
/** Initializing ID */
|
||||
|
|
@ -13,55 +12,100 @@ export const mUninitialized = new Set<Metro.ModuleID>()
|
|||
export const mInitialized = new Set<Metro.ModuleID>()
|
||||
|
||||
export const mImportedPaths = new Map<string, Metro.ModuleID>()
|
||||
export const mMetadata = new Map<Metro.ModuleID, Metadata>()
|
||||
export const mDeps: Metro.DependencyMap[] = []
|
||||
|
||||
export type Metadata = [
|
||||
deps: Metro.DependencyMap,
|
||||
initialized: boolean,
|
||||
module?: Metro.Module,
|
||||
]
|
||||
export let mList: Metro.ModuleList
|
||||
|
||||
export function patchMetroDefine(metroDefine: Metro.DefineFn) {
|
||||
return ((origFactory, id, deps) => {
|
||||
// deps won't be undefined last time I checked
|
||||
const metadata = [deps!, false] satisfies Metadata
|
||||
mMetadata.set(id, metadata)
|
||||
/**
|
||||
* Patching Metro's `__d` function to handle module definitions.
|
||||
*
|
||||
* Here's how Metro sets itself up:
|
||||
* 1. `__METRO_GLOBAL_PREFIX__ = ""`
|
||||
* 2. `${__METRO_GLOBAL_PREFIX__}__d = function define(...) {}`
|
||||
* - Why don't we patch it here? Because we need to know the value of __METRO_GLOBAL_PREFIX__ first
|
||||
* - And since Revenge runs before everything else, we need to patch in the next steps:
|
||||
* - We chose to patch it in 4. because we can access all the global functions at that point.
|
||||
* 3. `__r = function metroRequire(...) {}`
|
||||
* 4. `__c = function clear() {}`
|
||||
*
|
||||
* 5. `clear()`
|
||||
* - Metro clears the module list via clear(), and not __c() which makes it unpatchable.
|
||||
* 6. `__d(..., 0, [...])`
|
||||
* - The first module is defined with ID 0, which is the index module.
|
||||
* - Patch:
|
||||
* - We clear the module list again, so our module list is in sync with Metro's
|
||||
* #. `__d` is called with subsequent module definitions
|
||||
*/
|
||||
export function patchMetroDefine() {
|
||||
const defineKey = `${__METRO_GLOBAL_PREFIX__}__d` as const
|
||||
const metroDefine = globalThis[defineKey]
|
||||
|
||||
// Actual patched define function
|
||||
const defineRest: Metro.DefineFn = function define(factory, id, deps) {
|
||||
mDeps[id] = deps!
|
||||
mUninitialized.add(id)
|
||||
|
||||
metroDefine(
|
||||
((global, req, importDefault, importAll, module, exports, deps) => {
|
||||
handleFactoryCall(
|
||||
global,
|
||||
req,
|
||||
importDefault,
|
||||
importAll,
|
||||
module,
|
||||
exports,
|
||||
deps!,
|
||||
id,
|
||||
metadata,
|
||||
origFactory,
|
||||
)
|
||||
}) satisfies Metro.FactoryFn,
|
||||
(_, __, ___, ____, moduleObject) => {
|
||||
handleFactoryCall(factory, moduleObject)
|
||||
},
|
||||
id,
|
||||
deps,
|
||||
)
|
||||
}) satisfies Metro.DefineFn
|
||||
}
|
||||
|
||||
// Patched function for first __d call
|
||||
globalThis[defineKey] = function define(origFactory, id, deps) {
|
||||
// Clear the module list so we can keep in sync with Metro's
|
||||
mList = __c()
|
||||
|
||||
metroImportDefault = metroRequire.importDefault
|
||||
metroImportAll = metroRequire.importAll
|
||||
|
||||
// Call the actual patched function
|
||||
defineRest(origFactory, id, deps)
|
||||
// Set to the actual patched function so we can use it later
|
||||
globalThis[defineKey] = defineRest
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the module has bad exports. If it does, it will be "blacklisted" to avoid filtering issues.
|
||||
*
|
||||
* **Which module exports are considered bad?** Anything not an object or function, or an empty object.
|
||||
*
|
||||
* @param exports The exports of the module.
|
||||
*/
|
||||
export function isModuleExportsBad(exports: Metro.ModuleExports): boolean {
|
||||
return (
|
||||
// Nullish?
|
||||
exports == null ||
|
||||
// Isn't an object or function?
|
||||
// - Number exports are not useful, usually just an asset ID
|
||||
// - String, Boolean, Symbol, BigInt exports are not useful (who would do `module.exports = ...`?)
|
||||
!(
|
||||
(exports.__proto__ === Object.prototype &&
|
||||
Reflect.ownKeys(exports).length) ||
|
||||
exports.__proto__ === Function.prototype
|
||||
) ||
|
||||
// Can't run isProxy() on this because this isn't your typical proxy:
|
||||
// https://github.com/facebook/react-native/blob/master/packages/react-native/ReactCommon/react/nativemodule/core/ReactCommon/TurboModuleBinding.cpp
|
||||
exports === nativeModuleProxy
|
||||
)
|
||||
}
|
||||
|
||||
const global = globalThis
|
||||
const metroRequire = __r
|
||||
// We can't destructure here because metroRequire.* is set a little later
|
||||
let metroImportDefault: Metro.RequireFn
|
||||
let metroImportAll: Metro.RequireFn
|
||||
|
||||
function handleFactoryCall(
|
||||
global: object,
|
||||
req: Metro.RequireFn,
|
||||
importDefault: Metro.RequireFn,
|
||||
importAll: Metro.RequireFn,
|
||||
module: Metro.Module,
|
||||
exports: unknown,
|
||||
deps: Metro.ModuleID[],
|
||||
id: Metro.ModuleID,
|
||||
metadata: Metadata,
|
||||
factory: Metro.FactoryFn,
|
||||
moduleObject: Metro.Module,
|
||||
) {
|
||||
metadata[2] = module
|
||||
const exports = moduleObject.exports
|
||||
const id = moduleObject.id!
|
||||
|
||||
const prevIId = mInitializingId
|
||||
mInitializingId = id
|
||||
|
|
@ -69,15 +113,22 @@ function handleFactoryCall(
|
|||
executeRequireSubscriptions(id)
|
||||
|
||||
try {
|
||||
factory(global, req, importDefault, importAll, module, exports, deps)
|
||||
metadata[1] = true
|
||||
factory(
|
||||
global,
|
||||
metroRequire,
|
||||
metroImportDefault,
|
||||
metroImportAll,
|
||||
moduleObject,
|
||||
exports,
|
||||
mDeps[id],
|
||||
)
|
||||
|
||||
// Add the module to the initialized set only if the factory doesn't error or the exports aren't bad
|
||||
// Don't use exports here, as modules can set module.exports to a different object
|
||||
if (!isModuleExportsBad(module.exports)) mInitialized.add(id)
|
||||
executeInitializeSubscriptions(id, module.exports)
|
||||
} catch {}
|
||||
|
||||
mInitializingId = prevIId
|
||||
mUninitialized.delete(id)
|
||||
if (!isModuleExportsBad(moduleObject.exports)) mInitialized.add(id)
|
||||
executeInitializeSubscriptions(id, moduleObject.exports)
|
||||
} finally {
|
||||
mUninitialized.delete(mInitializingId)
|
||||
mInitializingId = prevIId
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { isProxy } from '@revenge-mod/utils/proxy'
|
||||
import { mMetadata } from './_internal'
|
||||
import { mDeps, mList } from './_internal'
|
||||
import type { Metro } from '../types'
|
||||
|
||||
/**
|
||||
|
|
@ -10,7 +10,7 @@ import type { Metro } from '../types'
|
|||
export function getModuleDependencies(
|
||||
id: Metro.ModuleID,
|
||||
): Metro.DependencyMap | undefined {
|
||||
return mMetadata.get(id)?.[0]
|
||||
return mDeps[id]
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -19,7 +19,7 @@ export function getModuleDependencies(
|
|||
* @returns `true` if the module is initialized, `false` if it is not initialized, or `undefined` if the module does not exist.
|
||||
*/
|
||||
export function isModuleInitialized(id: Metro.ModuleID): boolean | undefined {
|
||||
return mMetadata.get(id)?.[1]
|
||||
return mList.get(id)?.isInitialized
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -33,7 +33,7 @@ export function isModuleInitialized(id: Metro.ModuleID): boolean | undefined {
|
|||
export function getInitializedModuleExports(
|
||||
id: Metro.ModuleID,
|
||||
): Metro.ModuleExports | undefined {
|
||||
return mMetadata.get(id)?.[2]?.exports
|
||||
return mList.get(id)?.publicModule?.exports
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -53,27 +53,4 @@ export function isModuleExportBad(
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the module has bad exports. If it does, it will be "blacklisted" to avoid filtering issues.
|
||||
*
|
||||
* **Which module exports are considered bad?** Anything not an object or function, or an empty object.
|
||||
*
|
||||
* @param exports The exports of the module.
|
||||
*/
|
||||
export function isModuleExportsBad(exports: Metro.ModuleExports): boolean {
|
||||
return (
|
||||
// Nullish?
|
||||
exports == null ||
|
||||
// Isn't an object or function?
|
||||
// - Number exports are not useful, usually just an asset ID
|
||||
// - String, Boolean, Symbol, BigInt exports are not useful (who would do `module.exports = ...`?)
|
||||
!(
|
||||
(exports.__proto__ === Object.prototype &&
|
||||
Reflect.ownKeys(exports).length) ||
|
||||
exports.__proto__ === Function.prototype
|
||||
) ||
|
||||
// Can't run isProxy() on this because this isn't your typical proxy:
|
||||
// https://github.com/facebook/react-native/blob/master/packages/react-native/ReactCommon/react/nativemodule/core/ReactCommon/TurboModuleBinding.cpp
|
||||
exports === nativeModuleProxy
|
||||
)
|
||||
}
|
||||
export { isModuleExportsBad } from './_internal'
|
||||
|
|
|
|||
9
lib/types/types.d.ts
vendored
9
lib/types/types.d.ts
vendored
|
|
@ -69,9 +69,14 @@ declare global {
|
|||
/// METRO
|
||||
|
||||
declare global {
|
||||
var __r: Metro.RequireFn
|
||||
var __c: Metro.ClearFn
|
||||
var __METRO_GLOBAL_PREFIX__: ''
|
||||
|
||||
var __d: Metro.DefineFn
|
||||
var __r: Metro.RequireFn & {
|
||||
importDefault: Metro.ImportDefaultFn
|
||||
importAll: Metro.ImportAllFn
|
||||
}
|
||||
var __c: Metro.ClearFn
|
||||
}
|
||||
|
||||
/// REACT NATIVE
|
||||
|
|
|
|||
|
|
@ -3,6 +3,12 @@
|
|||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./patches/objects": {
|
||||
"default": "./src/patches/objects.ts"
|
||||
},
|
||||
"./patches/proxy": {
|
||||
"default": "./src/patches/proxy.ts"
|
||||
},
|
||||
"./callbacks": {
|
||||
"default": "./src/callbacks.ts"
|
||||
},
|
||||
|
|
@ -34,4 +40,4 @@
|
|||
"types": "./src/types.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,6 @@
|
|||
import { objectDefineProperty } from './patches/objects'
|
||||
import type { AnyObject } from './types'
|
||||
|
||||
// Retain reference to original functions
|
||||
export const objectFreeze = Object.freeze
|
||||
export const objectDefineProperty = Object.defineProperty
|
||||
|
||||
// Patch to prevent the use of Object.freeze
|
||||
Object.freeze = (o: any) => o
|
||||
|
||||
// Heavily inspired by uwu/shelter
|
||||
// https://github.com/uwu/shelter/blob/main/packages/shelter/src/exfiltrate.ts
|
||||
|
||||
|
|
|
|||
6
lib/utils/src/patches/objects.ts
Normal file
6
lib/utils/src/patches/objects.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// Retain reference to original functions
|
||||
export const objectFreeze = Object.freeze
|
||||
export const objectDefineProperty = Object.defineProperty
|
||||
|
||||
// Patch to prevent the use of Object.freeze
|
||||
Object.freeze = (o: any) => o
|
||||
10
lib/utils/src/patches/proxy.ts
Normal file
10
lib/utils/src/patches/proxy.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export const _targets = new WeakMap<object, object>()
|
||||
|
||||
const OriginalProxy = globalThis.Proxy
|
||||
globalThis.Proxy = new Proxy(OriginalProxy, {
|
||||
construct(_target, args) {
|
||||
const prox = new OriginalProxy(args[0], args[1])
|
||||
_targets.set(prox, args[0])
|
||||
return prox
|
||||
},
|
||||
})
|
||||
|
|
@ -4,17 +4,7 @@
|
|||
*/
|
||||
|
||||
import { getCurrentStack } from './errors'
|
||||
|
||||
const _targets = new WeakMap<object, object>()
|
||||
|
||||
const OriginalProxy = globalThis.Proxy
|
||||
globalThis.Proxy = new Proxy(OriginalProxy, {
|
||||
construct(_target, args) {
|
||||
const prox = new OriginalProxy(args[0], args[1])
|
||||
_targets.set(prox, args[0])
|
||||
return prox
|
||||
},
|
||||
})
|
||||
import { _targets } from './patches/proxy'
|
||||
|
||||
/**
|
||||
* Returns whether the object is a proxy.
|
||||
|
|
|
|||
85
src/index.ts
85
src/index.ts
|
|
@ -1,71 +1,18 @@
|
|||
// Note that both Object.freeze and Proxy patches are implicitly run by requiring the two libraries below.
|
||||
import { patchMetroDefine } from '@revenge-mod/modules/_/metro'
|
||||
import { interceptProperty } from '@revenge-mod/utils/objects'
|
||||
import type { Metro } from '@revenge-mod/modules/types'
|
||||
|
||||
// By doing this, we are assuming __METRO_GLOBAL_PREFIX__ is an empty string and globalThis.__d isn't already set.
|
||||
// But both should be true either way.
|
||||
const unintercept = interceptProperty('__d', (_, mDefine) => {
|
||||
unintercept()
|
||||
return patchMetroDefine(mDefine)
|
||||
Object.defineProperty(globalThis, '__c', {
|
||||
configurable: true,
|
||||
set(clear: Metro.ClearFn) {
|
||||
// @ts-expect-error
|
||||
// biome-ignore lint/performance/noDelete: We need to reset the property descriptor
|
||||
delete globalThis.__c
|
||||
globalThis.__c = clear
|
||||
|
||||
const { patchMetroDefine } =
|
||||
require('@revenge-mod/modules/_/metro') as typeof import('@revenge-mod/modules/_/metro')
|
||||
|
||||
patchMetroDefine()
|
||||
|
||||
require('./preinit')
|
||||
},
|
||||
})
|
||||
|
||||
import {
|
||||
onModuleFirstRequired,
|
||||
onModuleInitialized,
|
||||
} from '@revenge-mod/modules/metro/subscriptions'
|
||||
import { getErrorStack } from '@revenge-mod/utils/errors'
|
||||
|
||||
const IndexModuleId = 0
|
||||
|
||||
onModuleFirstRequired(IndexModuleId, function onIndexRequired() {
|
||||
try {
|
||||
if (__BUILD_FLAG_LOG_PROMISE_REJECTIONS__)
|
||||
require('./patches/log-promise-rejections')
|
||||
|
||||
// Initialize preinit libraries
|
||||
require('@revenge-mod/modules/preinit')
|
||||
require('@revenge-mod/react/preinit')
|
||||
require('@revenge-mod/assets/preinit')
|
||||
require('@revenge-mod/storage/preinit')
|
||||
require('@revenge-mod/discord/preinit')
|
||||
|
||||
// Run all preinit plugins
|
||||
require('~/plugins/preinit')
|
||||
require('@revenge-mod/plugins/preinit')
|
||||
|
||||
onModuleInitialized(IndexModuleId, function onIndexInitialized() {
|
||||
try {
|
||||
// Initialize init libraries
|
||||
require('@revenge-mod/storage/init')
|
||||
require('@revenge-mod/externals/init')
|
||||
require('@revenge-mod/discord/init')
|
||||
require('@revenge-mod/components/init')
|
||||
|
||||
// Run all init plugins
|
||||
require('~/plugins/init')
|
||||
require('@revenge-mod/plugins/init')
|
||||
} catch (e) {
|
||||
onError(e)
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
onError(e)
|
||||
}
|
||||
})
|
||||
|
||||
function onError(e: unknown) {
|
||||
// TODO(init): Move to use native provided alert function, which will accept a string stack trace
|
||||
// Above will reduce the need for runCatching() to exist, as the code won't be able to be deduped any further
|
||||
const { ClientInfoModule, DeviceModule } =
|
||||
require('@revenge-mod/discord/native') as typeof import('@revenge-mod/discord/native')
|
||||
|
||||
const Client = ClientInfoModule.getConstants()
|
||||
const Device = DeviceModule.getConstants()
|
||||
|
||||
alert(
|
||||
`Failed to load Revenge (${__BUILD_VERSION__}-${__BUILD_COMMIT__}-${__BUILD_BRANCH__} (${__BUILD_ENV__}))\n` +
|
||||
`Discord: ${Client.Version} (${Client.Build})\n` +
|
||||
`Device: ${Device.deviceManufacturer} ${Device.deviceModel}\n\n` +
|
||||
getErrorStack(e),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
63
src/preinit.ts
Normal file
63
src/preinit.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import '@revenge-mod/utils/patches/proxy'
|
||||
import '@revenge-mod/utils/patches/objects'
|
||||
|
||||
import {
|
||||
onModuleFirstRequired,
|
||||
onModuleInitialized,
|
||||
} from '@revenge-mod/modules/metro/subscriptions'
|
||||
import { getErrorStack } from '@revenge-mod/utils/errors'
|
||||
|
||||
const IndexModuleId = 0
|
||||
|
||||
onModuleFirstRequired(IndexModuleId, function onIndexRequired() {
|
||||
try {
|
||||
if (__BUILD_FLAG_LOG_PROMISE_REJECTIONS__)
|
||||
require('./patches/log-promise-rejections')
|
||||
|
||||
// Initialize preinit libraries
|
||||
require('@revenge-mod/modules/preinit')
|
||||
require('@revenge-mod/react/preinit')
|
||||
require('@revenge-mod/assets/preinit')
|
||||
require('@revenge-mod/storage/preinit')
|
||||
require('@revenge-mod/discord/preinit')
|
||||
|
||||
// Run all preinit plugins
|
||||
require('~/plugins/preinit')
|
||||
require('@revenge-mod/plugins/preinit')
|
||||
|
||||
onModuleInitialized(IndexModuleId, function onIndexInitialized() {
|
||||
try {
|
||||
// Initialize init libraries
|
||||
require('@revenge-mod/storage/init')
|
||||
require('@revenge-mod/externals/init')
|
||||
require('@revenge-mod/discord/init')
|
||||
require('@revenge-mod/components/init')
|
||||
|
||||
// Run all init plugins
|
||||
require('~/plugins/init')
|
||||
require('@revenge-mod/plugins/init')
|
||||
} catch (e) {
|
||||
onError(e)
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
onError(e)
|
||||
}
|
||||
})
|
||||
|
||||
function onError(e: unknown) {
|
||||
// TODO(init): Move to use native provided alert function, which will accept a string stack trace
|
||||
// Above will reduce the need for runCatching() to exist, as the code won't be able to be deduped any further
|
||||
const { ClientInfoModule, DeviceModule } =
|
||||
require('@revenge-mod/discord/native') as typeof import('@revenge-mod/discord/native')
|
||||
|
||||
const Client = ClientInfoModule.getConstants()
|
||||
const Device = DeviceModule.getConstants()
|
||||
|
||||
alert(
|
||||
`Failed to load Revenge (${__BUILD_VERSION__}-${__BUILD_COMMIT__}-${__BUILD_BRANCH__} (${__BUILD_ENV__}))\n` +
|
||||
`Discord: ${Client.Version} (${Client.Build})\n` +
|
||||
`Device: ${Device.deviceManufacturer} ${Device.deviceModel}\n\n` +
|
||||
getErrorStack(e),
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue