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:
PalmDevs 2025-07-07 19:32:58 +07:00
parent 0bea1e29be
commit e9fedc8db4
No known key found for this signature in database
GPG key ID: 6FAE8F8677ACA8BA
11 changed files with 215 additions and 166 deletions

View file

@ -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)

View file

@ -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
}
}

View file

@ -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'

View file

@ -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

View file

@ -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"
}
}
}
}

View file

@ -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

View 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

View 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
},
})

View file

@ -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.

View file

@ -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
View 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),
)
}