@@ -39,6 +39,10 @@ public static void RestoreInstance(JsRuntimeManager vm)
3939 readonly Dictionary < FixedString32Bytes , string > m_StringCache32 = new ( ) ;
4040 readonly Dictionary < string , byte [ ] > m_EncodedStringCache = new ( ) ;
4141
42+ // Cache: (scriptName, funcNameBytes ref) → JS function JSValue.
43+ // Avoids repeated JS_GetPropertyStr per entity per frame.
44+ readonly Dictionary < ( string , byte [ ] ) , JSValue > m_FuncCache = new ( ) ;
45+
4246 // ── Sub-objects ──
4347
4448 public JsModuleManager Modules { get ; }
@@ -143,10 +147,17 @@ public bool LoadScriptFromString(string scriptId, string source) =>
143147 public unsafe bool LoadScriptAsModule ( string scriptId , string source , string filename ) =>
144148 Modules . LoadAsModule ( scriptId , source , filename ) ;
145149
146- public unsafe bool ReloadScript ( string scriptName , string source , string filename ) =>
147- Modules . Reload ( scriptName , source , filename ) ;
150+ public unsafe bool ReloadScript ( string scriptName , string source , string filename )
151+ {
152+ InvalidateFuncCache ( scriptName ) ;
153+ return Modules . Reload ( scriptName , source , filename ) ;
154+ }
148155
149- public bool SimulateHotReload ( string scriptName ) => Modules . SimulateHotReload ( scriptName ) ;
156+ public bool SimulateHotReload ( string scriptName )
157+ {
158+ InvalidateFuncCache ( scriptName ) ;
159+ return Modules . SimulateHotReload ( scriptName ) ;
160+ }
150161
151162 public unsafe string VerifyModuleHealth ( ) => Modules . VerifyHealth ( ) ;
152163
@@ -174,37 +185,50 @@ string errorContext
174185 if ( ! Modules . ScriptRefs . TryGetValue ( scriptName , out var scriptObj ) )
175186 return false ;
176187
177- fixed ( byte * pFuncName = funcNameBytes )
188+ // Try cached function lookup first — avoids JS_GetPropertyStr P/Invoke per call.
189+ var cacheKey = ( scriptName , funcNameBytes ) ;
190+ if ( ! m_FuncCache . TryGetValue ( cacheKey , out var func ) )
178191 {
179- var func = QJS . JS_GetPropertyStr ( m_Context , scriptObj , pFuncName ) ;
192+ fixed ( byte * pFuncName = funcNameBytes )
193+ func = QJS . JS_GetPropertyStr ( m_Context , scriptObj , pFuncName ) ;
194+
180195 if ( QJS . JS_IsFunction ( m_Context , func ) == 0 )
181196 {
182197 QJS . JS_FreeValue ( m_Context , func ) ;
198+ // Cache a sentinel so we don't re-lookup non-function exports.
199+ m_FuncCache [ cacheKey ] = QJS . JS_UNDEFINED ;
183200 return true ;
184201 }
185202
186- if ( ! States . Refs . TryGetValue ( stateRef , out var stateVal ) )
187- stateVal = QJS . JS_UNDEFINED ;
203+ // DupValue to keep the function alive across frames.
204+ m_FuncCache [ cacheKey ] = QJS . JS_DupValue ( m_Context , func ) ;
205+ QJS . JS_FreeValue ( m_Context , func ) ;
206+ func = m_FuncCache [ cacheKey ] ;
207+ }
208+
209+ // Sentinel: export exists but is not a function — skip silently.
210+ if ( QJS . JS_IsFunction ( m_Context , func ) == 0 )
211+ return true ;
188212
189- var totalArgc = 1 + extraArgc ;
190- var argv = stackalloc JSValue [ totalArgc ] ;
191- argv [ 0 ] = stateVal ;
192- for ( var i = 0 ; i < extraArgc ; i ++ )
193- argv [ 1 + i ] = extraArgv [ i ] ;
213+ if ( ! States . Refs . TryGetValue ( stateRef , out var stateVal ) )
214+ stateVal = QJS . JS_UNDEFINED ;
194215
195- var result = QJS . JS_Call ( m_Context , func , scriptObj , totalArgc , argv ) ;
196- if ( QJS . IsException ( result ) )
197- {
198- LogException ( errorContext ) ;
199- QJS . JS_FreeValue ( m_Context , result ) ;
200- QJS . JS_FreeValue ( m_Context , func ) ;
201- return false ;
202- }
216+ var totalArgc = 1 + extraArgc ;
217+ var argv = stackalloc JSValue [ totalArgc ] ;
218+ argv [ 0 ] = stateVal ;
219+ for ( var i = 0 ; i < extraArgc ; i ++ )
220+ argv [ 1 + i ] = extraArgv [ i ] ;
203221
222+ var result = QJS . JS_Call ( m_Context , func , scriptObj , totalArgc , argv ) ;
223+ if ( QJS . IsException ( result ) )
224+ {
225+ LogException ( errorContext ) ;
204226 QJS . JS_FreeValue ( m_Context , result ) ;
205- QJS . JS_FreeValue ( m_Context , func ) ;
206- return true ;
227+ return false ;
207228 }
229+
230+ QJS . JS_FreeValue ( m_Context , result ) ;
231+ return true ;
208232 }
209233
210234 public unsafe bool CallFunction ( string scriptName , string funcName , int stateRef )
@@ -410,6 +434,36 @@ public unsafe JSValue EvalGlobal(JSContext ctx, string code, string filename)
410434 return QJS . EvalGlobal ( ctx , code , filename ) ;
411435 }
412436
437+ // ── Function cache ──
438+
439+ void ClearFuncCache ( )
440+ {
441+ if ( ! m_Context . IsNull )
442+ {
443+ foreach ( var kv in m_FuncCache )
444+ if ( QJS . JS_IsFunction ( m_Context , kv . Value ) != 0 )
445+ QJS . JS_FreeValue ( m_Context , kv . Value ) ;
446+ }
447+ m_FuncCache . Clear ( ) ;
448+ }
449+
450+ void InvalidateFuncCache ( string scriptName )
451+ {
452+ // Remove all cached functions for this specific module.
453+ var toRemove = new List < ( string , byte [ ] ) > ( ) ;
454+ foreach ( var kv in m_FuncCache )
455+ if ( kv . Key . Item1 == scriptName )
456+ toRemove . Add ( kv . Key ) ;
457+
458+ foreach ( var key in toRemove )
459+ {
460+ if ( ! m_Context . IsNull && m_FuncCache . TryGetValue ( key , out var val ) )
461+ if ( QJS . JS_IsFunction ( m_Context , val ) != 0 )
462+ QJS . JS_FreeValue ( m_Context , val ) ;
463+ m_FuncCache . Remove ( key ) ;
464+ }
465+ }
466+
413467 // ── Dispose ──
414468
415469 readonly List < Action < JSContext > > m_PreDisposeCallbacks = new ( ) ;
@@ -448,6 +502,7 @@ public void Dispose()
448502 m_StringCache . Clear ( ) ;
449503 m_StringCache32 . Clear ( ) ;
450504 m_EncodedStringCache . Clear ( ) ;
505+ ClearFuncCache ( ) ;
451506
452507 States . DisposeAll ( ) ;
453508 Modules . DisposeAll ( ) ;
0 commit comments