1 | // Licensed to the .NET Foundation under one or more agreements. |
2 | // The .NET Foundation licenses this file to you under the MIT license. |
3 | // See the LICENSE file in the project root for more information. |
4 | // |
5 | // ReJit.cpp |
6 | // |
7 | |
8 | // |
9 | // This module implements the tracking and execution of rejit requests. In order to avoid |
10 | // any overhead on the non-profiled case we don't intrude on any 'normal' data structures |
11 | // except one member on the AppDomain to hold our main hashtable and crst (the |
12 | // ReJitManager). See comments in rejit.h to understand relationships between ReJitInfo, |
13 | // SharedReJitInfo, and ReJitManager, particularly SharedReJitInfo::InternalFlags which |
14 | // capture the state of a rejit request, and ReJitInfo::InternalFlags which captures the |
15 | // state of a particular MethodDesc from a rejit request. |
16 | // |
17 | // A ReJIT request (tracked via SharedReJitInfo) is made at the level of a (Module *, |
18 | // methodDef) pair, and thus affects all instantiations of a generic. Each MethodDesc |
19 | // affected by a ReJIT request has its state tracked via a ReJitInfo instance. A |
20 | // ReJitInfo can represent a rejit request against an already-jitted MethodDesc, or a |
21 | // rejit request against a not-yet-jitted MethodDesc (called a "pre-rejit" request). A |
22 | // Pre-ReJIT request happens when a profiler specifies a (Module *, methodDef) pair that |
23 | // has not yet been JITted, or that represents a generic function which always has the |
24 | // potential to JIT new instantiations in the future. |
25 | // |
26 | // Top-level functions in this file of most interest are: |
27 | // |
28 | // * (static) code:ReJitManager::RequestReJIT: |
29 | // Profiling API just delegates all rejit requests directly to this function. It is |
30 | // responsible for recording the request into the appropriate ReJITManagers and for |
31 | // jump-stamping any already-JITted functions affected by the request (so that future |
32 | // calls hit the prestub) |
33 | // |
34 | // * code:ReJitManager::DoReJitIfNecessary: |
35 | // MethodDesc::DoPrestub calls this to determine whether it's been invoked to do a rejit. |
36 | // If so, ReJitManager::DoReJitIfNecessary is responsible for (indirectly) gathering the |
37 | // appropriate IL and codegen flags, calling UnsafeJitFunction(), and redirecting the |
38 | // jump-stamp from the prestub to the newly-rejitted code. |
39 | // |
40 | // * code:PublishMethodHolder::PublishMethodHolder |
41 | // MethodDesc::MakeJitWorker() calls this to determine if there's an outstanding |
42 | // "pre-rejit" request for a MethodDesc that has just been jitted for the first time. We |
43 | // also call this from MethodDesc::CheckRestore when restoring generic methods. |
44 | // The holder applies the jump-stamp to the |
45 | // top of the originally JITted code, with the jump target being the prestub. |
46 | // When ReJIT is enabled this holder enters the ReJIT |
47 | // lock to enforce atomicity of doing the pre-rejit-jmp-stamp & publishing/restoring |
48 | // the PCODE, which is required to avoid races with a profiler that calls RequestReJIT |
49 | // just as the method finishes compiling/restoring. |
50 | // |
51 | // * code:PublishMethodTableHolder::PublishMethodTableHolder |
52 | // Does the same thing as PublishMethodHolder except iterating over every |
53 | // method in the MethodTable. This is called from MethodTable::SetIsRestored. |
54 | // |
55 | // * code:ReJitManager::GetCurrentReJitFlags: |
56 | // CEEInfo::canInline() calls this as part of its calculation of whether it may inline a |
57 | // given method. (Profilers may specify on a per-rejit-request basis whether the rejit of |
58 | // a method may inline callees.) |
59 | // |
60 | // |
61 | // #Invariants: |
62 | // |
63 | // For a given Module/MethodDef there is at most 1 SharedReJitInfo that is not Reverted, |
64 | // though there may be many that are in the Reverted state. If a method is rejitted |
65 | // multiple times, with multiple versions actively in use on the stacks, then all but the |
66 | // most recent are put into the Reverted state even though they may not yet be physically |
67 | // reverted and pitched yet. |
68 | // |
69 | // For a given MethodDesc there is at most 1 ReJitInfo in the kJumpToPrestub or kJumpToRejittedCode |
70 | // state. |
71 | // |
72 | // The ReJitManager::m_crstTable lock is held whenever reading or writing to that |
73 | // ReJitManager instance's table (including state transitions applied to the ReJitInfo & |
74 | // SharedReJitInfo instances stored in that table). |
75 | // |
76 | // The ReJitManager::m_crstTable lock is never held during callbacks to the profiler |
77 | // such as GetReJITParameters, ReJITStarted, JITComplete, ReportReJITError |
78 | // |
79 | // Any thread holding the ReJitManager::m_crstTable lock can't block during runtime suspension |
80 | // therefore it can't call any GC_TRIGGERS functions |
81 | // |
82 | // Transitions between SharedRejitInfo states happen only in the following cicumstances: |
83 | // 1) New SharedRejitInfo added to table (Requested State) |
84 | // Inside RequestRejit |
85 | // Global Crst held, table Crst held |
86 | // |
87 | // 2) Requested -> GettingReJITParameters |
88 | // Inside DoRejitIfNecessary |
89 | // Global Crst NOT held, table Crst held |
90 | // |
91 | // 3) GettingReJITParameters -> Active |
92 | // Inside DoRejitIfNecessary |
93 | // Global Crst NOT held, table Crst held |
94 | // |
95 | // 4) * -> Reverted |
96 | // Inside RequestRejit or RequestRevert |
97 | // Global Crst held, table Crst held |
98 | // |
99 | // |
100 | // Transitions between RejitInfo states happen only in the following circumstances: |
101 | // 1) New RejitInfo added to table (kJumpNone state) |
102 | // Inside RequestRejit, DoJumpStampIfNecessary |
103 | // Global Crst MAY/MAY NOT be held, table Crst held |
104 | // Allowed SharedReJit states: Requested, GettingReJITParameters, Active |
105 | // |
106 | // 2) kJumpNone -> kJumpToPrestub |
107 | // Inside RequestRejit, DoJumpStampIfNecessary |
108 | // Global Crst MAY/MAY NOT be held, table Crst held |
109 | // Allowed SharedReJit states: Requested, GettingReJITParameters, Active |
110 | // |
111 | // 3) kJumpToPreStub -> kJumpToRejittedCode |
112 | // Inside DoReJitIfNecessary |
113 | // Global Crst NOT held, table Crst held |
114 | // Allowed SharedReJit states: Active |
115 | // |
116 | // 4) * -> kJumpNone |
117 | // Inside RequestRevert, RequestRejit |
118 | // Global Crst held, table crst held |
119 | // Allowed SharedReJit states: Reverted |
120 | // |
121 | // |
122 | // #Beware Invariant misconceptions - don't make bad assumptions! |
123 | // Even if a SharedReJitInfo is in the Reverted state: |
124 | // a) RejitInfos may still be in the kJumpToPreStub or kJumpToRejittedCode state |
125 | // Reverted really just means the runtime has started reverting, but it may not |
126 | // be complete yet on the thread executing Revert or RequestRejit. |
127 | // b) The code for this version of the method may be executing on any number of |
128 | // threads. Even after transitioning all rejit infos to kJumpNone state we |
129 | // have no power to abort or hijack threads already running the rejitted code. |
130 | // |
131 | // Even if a SharedReJitInfo is in the Active state: |
132 | // a) The corresponding ReJitInfos may not be jump-stamped yet. |
133 | // Some thread is still in the progress of getting this thread jump-stamped |
134 | // OR it is a place-holder ReJitInfo. |
135 | // b) An older ReJitInfo linked to a reverted SharedReJitInfo could still be |
136 | // in kJumpToPreStub or kJumpToReJittedCode state. RequestRejit is still in |
137 | // progress on some thread. |
138 | // |
139 | // |
140 | // #Known issues with REJIT at this time: |
141 | // NGEN inlined methods will not be properly rejitted |
142 | // Exception callstacks through rejitted code do not produce correct StackTraces |
143 | // Live debugging is not supported when rejit is enabled |
144 | // Rejit leaks rejitted methods, RejitInfos, and SharedRejitInfos until AppDomain unload |
145 | // Dump debugging doesn't correctly locate RejitInfos that are keyed by MethodDesc |
146 | // Metadata update creates large memory increase switching to RW (not specifically a rejit issue) |
147 | // |
148 | // ====================================================================================== |
149 | |
150 | #include "common.h" |
151 | #include "rejit.h" |
152 | #include "method.hpp" |
153 | #include "eeconfig.h" |
154 | #include "methoditer.h" |
155 | #include "dbginterface.h" |
156 | #include "threadsuspend.h" |
157 | |
158 | #ifdef FEATURE_REJIT |
159 | #ifdef FEATURE_CODE_VERSIONING |
160 | |
161 | #include "../debug/ee/debugger.h" |
162 | #include "../debug/ee/walker.h" |
163 | #include "../debug/ee/controller.h" |
164 | #include "codeversion.h" |
165 | |
166 | // This HRESULT is only used as a private implementation detail. Corerror.xml has a comment in it |
167 | // reserving this value for our use but it doesn't appear in the public headers. |
168 | #define CORPROF_E_RUNTIME_SUSPEND_REQUIRED 0x80131381 |
169 | |
170 | // This is just used as a unique id. Overflow is OK. If we happen to have more than 4+Billion rejits |
171 | // and somehow manage to not run out of memory, we'll just have to redefine ReJITID as size_t. |
172 | /* static */ |
173 | static ReJITID s_GlobalReJitId = 1; |
174 | |
175 | /* static */ |
176 | CrstStatic ReJitManager::s_csGlobalRequest; |
177 | |
178 | |
179 | //--------------------------------------------------------------------------------------- |
180 | // Helpers |
181 | |
182 | //static |
183 | CORJIT_FLAGS ReJitManager::JitFlagsFromProfCodegenFlags(DWORD dwCodegenFlags) |
184 | { |
185 | LIMITED_METHOD_DAC_CONTRACT; |
186 | |
187 | CORJIT_FLAGS jitFlags; |
188 | if ((dwCodegenFlags & COR_PRF_CODEGEN_DISABLE_ALL_OPTIMIZATIONS) != 0) |
189 | { |
190 | jitFlags.Set(CORJIT_FLAGS::CORJIT_FLAG_DEBUG_CODE); |
191 | } |
192 | if ((dwCodegenFlags & COR_PRF_CODEGEN_DISABLE_INLINING) != 0) |
193 | { |
194 | jitFlags.Set(CORJIT_FLAGS::CORJIT_FLAG_NO_INLINING); |
195 | } |
196 | |
197 | // In the future more flags may be added that need to be converted here (e.g., |
198 | // COR_PRF_CODEGEN_ENTERLEAVE / CORJIT_FLAG_PROF_ENTERLEAVE) |
199 | |
200 | return jitFlags; |
201 | } |
202 | |
203 | //--------------------------------------------------------------------------------------- |
204 | // ProfilerFunctionControl implementation |
205 | |
206 | ProfilerFunctionControl::ProfilerFunctionControl(LoaderHeap * pHeap) : |
207 | m_refCount(1), |
208 | m_pHeap(pHeap), |
209 | m_dwCodegenFlags(0), |
210 | m_cbIL(0), |
211 | m_pbIL(NULL), |
212 | m_cInstrumentedMapEntries(0), |
213 | m_rgInstrumentedMapEntries(NULL) |
214 | { |
215 | LIMITED_METHOD_CONTRACT; |
216 | } |
217 | |
218 | ProfilerFunctionControl::~ProfilerFunctionControl() |
219 | { |
220 | LIMITED_METHOD_CONTRACT; |
221 | |
222 | // Intentionally not deleting m_pbIL or m_rgInstrumentedMapEntries, as its ownership gets transferred to the |
223 | // SharedReJitInfo that manages that rejit request. |
224 | } |
225 | |
226 | |
227 | HRESULT ProfilerFunctionControl::QueryInterface(REFIID id, void** pInterface) |
228 | { |
229 | LIMITED_METHOD_CONTRACT; |
230 | |
231 | if ((id != IID_IUnknown) && |
232 | (id != IID_ICorProfilerFunctionControl)) |
233 | { |
234 | *pInterface = NULL; |
235 | return E_NOINTERFACE; |
236 | } |
237 | |
238 | *pInterface = this; |
239 | this->AddRef(); |
240 | return S_OK; |
241 | } |
242 | |
243 | ULONG ProfilerFunctionControl::AddRef() |
244 | { |
245 | LIMITED_METHOD_CONTRACT; |
246 | |
247 | return InterlockedIncrement(&m_refCount); |
248 | } |
249 | |
250 | ULONG ProfilerFunctionControl::Release() |
251 | { |
252 | LIMITED_METHOD_CONTRACT; |
253 | |
254 | ULONG refCount = InterlockedDecrement(&m_refCount); |
255 | |
256 | if (0 == refCount) |
257 | { |
258 | delete this; |
259 | } |
260 | |
261 | return refCount; |
262 | } |
263 | |
264 | //--------------------------------------------------------------------------------------- |
265 | // |
266 | // Profiler calls this to specify a set of flags from COR_PRF_CODEGEN_FLAGS |
267 | // to control rejitting a particular methodDef. |
268 | // |
269 | // Arguments: |
270 | // * flags - set of flags from COR_PRF_CODEGEN_FLAGS |
271 | // |
272 | // Return Value: |
273 | // Always S_OK; |
274 | // |
275 | |
276 | HRESULT ProfilerFunctionControl::SetCodegenFlags(DWORD flags) |
277 | { |
278 | LIMITED_METHOD_CONTRACT; |
279 | |
280 | m_dwCodegenFlags = flags; |
281 | return S_OK; |
282 | } |
283 | |
284 | //--------------------------------------------------------------------------------------- |
285 | // |
286 | // Profiler calls this to specify the IL to use when rejitting a particular methodDef. |
287 | // |
288 | // Arguments: |
289 | // * cbNewILMethodHeader - Size in bytes of pbNewILMethodHeader |
290 | // * pbNewILMethodHeader - Pointer to beginning of IL header + IL bytes. |
291 | // |
292 | // Return Value: |
293 | // HRESULT indicating success or failure. |
294 | // |
295 | // Notes: |
296 | // Caller owns allocating and freeing pbNewILMethodHeader as expected. |
297 | // SetILFunctionBody copies pbNewILMethodHeader into a separate buffer. |
298 | // |
299 | |
300 | HRESULT ProfilerFunctionControl::SetILFunctionBody(ULONG , LPCBYTE ) |
301 | { |
302 | CONTRACTL |
303 | { |
304 | NOTHROW; |
305 | GC_NOTRIGGER; |
306 | MODE_ANY; |
307 | } |
308 | CONTRACTL_END; |
309 | |
310 | if (cbNewILMethodHeader == 0) |
311 | { |
312 | return E_INVALIDARG; |
313 | } |
314 | |
315 | if (pbNewILMethodHeader == NULL) |
316 | { |
317 | return E_INVALIDARG; |
318 | } |
319 | |
320 | _ASSERTE(m_cbIL == 0); |
321 | _ASSERTE(m_pbIL == NULL); |
322 | |
323 | #ifdef DACCESS_COMPILE |
324 | m_pbIL = new (nothrow) BYTE[cbNewILMethodHeader]; |
325 | #else |
326 | // IL is stored on the appropriate loader heap, and its memory will be owned by the |
327 | // SharedReJitInfo we copy the pointer to. |
328 | m_pbIL = (LPBYTE) (void *) m_pHeap->AllocMem_NoThrow(S_SIZE_T(cbNewILMethodHeader)); |
329 | #endif |
330 | if (m_pbIL == NULL) |
331 | { |
332 | return E_OUTOFMEMORY; |
333 | } |
334 | |
335 | m_cbIL = cbNewILMethodHeader; |
336 | memcpy(m_pbIL, pbNewILMethodHeader, cbNewILMethodHeader); |
337 | |
338 | return S_OK; |
339 | } |
340 | |
341 | HRESULT ProfilerFunctionControl::SetILInstrumentedCodeMap(ULONG cILMapEntries, COR_IL_MAP * rgILMapEntries) |
342 | { |
343 | #ifdef DACCESS_COMPILE |
344 | // I'm not sure why any of these methods would need to be compiled in DAC? Could we remove the |
345 | // entire class from the DAC'ized code build? |
346 | _ASSERTE(!"This shouldn't be called in DAC" ); |
347 | return E_NOTIMPL; |
348 | #else |
349 | |
350 | CONTRACTL |
351 | { |
352 | NOTHROW; |
353 | GC_NOTRIGGER; |
354 | MODE_ANY; |
355 | } |
356 | CONTRACTL_END; |
357 | |
358 | if (cILMapEntries >= (MAXULONG / sizeof(COR_IL_MAP))) |
359 | { |
360 | // Too big! The allocation below would overflow when calculating the size. |
361 | return E_INVALIDARG; |
362 | } |
363 | |
364 | if (g_pDebugInterface == NULL) |
365 | { |
366 | return CORPROF_E_DEBUGGING_DISABLED; |
367 | } |
368 | |
369 | |
370 | // copy the il map and il map entries into the corresponding fields. |
371 | m_cInstrumentedMapEntries = cILMapEntries; |
372 | |
373 | // IL is stored on the appropriate loader heap, and its memory will be owned by the |
374 | // SharedReJitInfo we copy the pointer to. |
375 | m_rgInstrumentedMapEntries = (COR_IL_MAP*) (void *) m_pHeap->AllocMem_NoThrow(S_SIZE_T(cILMapEntries * sizeof(COR_IL_MAP))); |
376 | |
377 | if (m_rgInstrumentedMapEntries == NULL) |
378 | return E_OUTOFMEMORY; |
379 | |
380 | |
381 | memcpy_s(m_rgInstrumentedMapEntries, sizeof(COR_IL_MAP) * cILMapEntries, rgILMapEntries, sizeof(COR_IL_MAP) * cILMapEntries); |
382 | |
383 | return S_OK; |
384 | #endif // DACCESS_COMPILE |
385 | } |
386 | |
387 | //--------------------------------------------------------------------------------------- |
388 | // |
389 | // ReJitManager may use this to access the codegen flags the profiler had set on this |
390 | // ICorProfilerFunctionControl. |
391 | // |
392 | // Return Value: |
393 | // * codegen flags previously set via SetCodegenFlags; 0 if none were set. |
394 | // |
395 | DWORD ProfilerFunctionControl::GetCodegenFlags() |
396 | { |
397 | return m_dwCodegenFlags; |
398 | } |
399 | |
400 | //--------------------------------------------------------------------------------------- |
401 | // |
402 | // ReJitManager may use this to access the IL header + instructions the |
403 | // profiler had set on this ICorProfilerFunctionControl via SetIL |
404 | // |
405 | // Return Value: |
406 | // * Pointer to ProfilerFunctionControl-allocated buffer containing the |
407 | // IL header and instructions the profiler had provided. |
408 | // |
409 | LPBYTE ProfilerFunctionControl::GetIL() |
410 | { |
411 | return m_pbIL; |
412 | } |
413 | |
414 | //--------------------------------------------------------------------------------------- |
415 | // |
416 | // ReJitManager may use this to access the count of instrumented map entry flags the |
417 | // profiler had set on this ICorProfilerFunctionControl. |
418 | // |
419 | // Return Value: |
420 | // * size of the instrumented map entry array |
421 | // |
422 | ULONG ProfilerFunctionControl::GetInstrumentedMapEntryCount() |
423 | { |
424 | return m_cInstrumentedMapEntries; |
425 | } |
426 | |
427 | //--------------------------------------------------------------------------------------- |
428 | // |
429 | // ReJitManager may use this to access the instrumented map entries the |
430 | // profiler had set on this ICorProfilerFunctionControl. |
431 | // |
432 | // Return Value: |
433 | // * the array of instrumented map entries |
434 | // |
435 | COR_IL_MAP* ProfilerFunctionControl::GetInstrumentedMapEntries() |
436 | { |
437 | return m_rgInstrumentedMapEntries; |
438 | } |
439 | |
440 | //--------------------------------------------------------------------------------------- |
441 | // ReJitManager implementation |
442 | |
443 | // All the state-changey stuff is kept up here in the !DACCESS_COMPILE block. |
444 | // The more read-only inspection-y stuff follows the block. |
445 | |
446 | #ifndef DACCESS_COMPILE |
447 | |
448 | //--------------------------------------------------------------------------------------- |
449 | // |
450 | // ICorProfilerInfo4::RequestReJIT calls into this guy to do most of the |
451 | // work. Takes care of finding the appropriate ReJitManager instances to |
452 | // record the rejit requests and perform jmp-stamping. |
453 | // |
454 | // Arguments: |
455 | // * cFunctions - Element count of rgModuleIDs & rgMethodDefs |
456 | // * rgModuleIDs - Parallel array of ModuleIDs to rejit |
457 | // * rgMethodDefs - Parallel array of methodDefs to rejit |
458 | // |
459 | // Return Value: |
460 | // HRESULT indicating success or failure of the overall operation. Each |
461 | // individual methodDef (or MethodDesc associated with the methodDef) |
462 | // may encounter its own failure, which is reported by the ReJITError() |
463 | // callback, which is called into the profiler directly. |
464 | // |
465 | |
466 | // static |
467 | HRESULT ReJitManager::RequestReJIT( |
468 | ULONG cFunctions, |
469 | ModuleID rgModuleIDs[], |
470 | mdMethodDef rgMethodDefs[]) |
471 | { |
472 | return ReJitManager::UpdateActiveILVersions(cFunctions, rgModuleIDs, rgMethodDefs, NULL, FALSE); |
473 | } |
474 | |
475 | |
476 | // static |
477 | HRESULT ReJitManager::UpdateActiveILVersions( |
478 | ULONG cFunctions, |
479 | ModuleID rgModuleIDs[], |
480 | mdMethodDef rgMethodDefs[], |
481 | HRESULT rgHrStatuses[], |
482 | BOOL fIsRevert) |
483 | { |
484 | CONTRACTL |
485 | { |
486 | NOTHROW; |
487 | GC_TRIGGERS; |
488 | CAN_TAKE_LOCK; |
489 | MODE_PREEMPTIVE; |
490 | } |
491 | CONTRACTL_END; |
492 | |
493 | // Serialize all RequestReJIT() and Revert() calls against each other (even across AppDomains) |
494 | CrstHolder ch(&(s_csGlobalRequest)); |
495 | |
496 | HRESULT hr = S_OK; |
497 | |
498 | // Request at least 1 method to reJIT! |
499 | _ASSERTE ((cFunctions != 0) && (rgModuleIDs != NULL) && (rgMethodDefs != NULL)); |
500 | |
501 | // Temporary storage to batch up all the ReJitInfos that will get jump stamped |
502 | // later when the runtime is suspended. |
503 | // |
504 | //DESKTOP WARNING: On CoreCLR we are safe but if this code ever gets ported back |
505 | //there aren't any protections against domain unload. Any of these moduleIDs |
506 | //code version managers, or code versions would become invalid if the domain which |
507 | //contains them was unloaded. |
508 | SHash<CodeActivationBatchTraits> mgrToCodeActivationBatch; |
509 | CDynArray<CodeVersionManager::CodePublishError> errorRecords; |
510 | for (ULONG i = 0; i < cFunctions; i++) |
511 | { |
512 | Module * pModule = reinterpret_cast< Module * >(rgModuleIDs[i]); |
513 | if (pModule == NULL || TypeFromToken(rgMethodDefs[i]) != mdtMethodDef) |
514 | { |
515 | ReportReJITError(pModule, rgMethodDefs[i], NULL, E_INVALIDARG); |
516 | continue; |
517 | } |
518 | |
519 | if (pModule->IsBeingUnloaded()) |
520 | { |
521 | ReportReJITError(pModule, rgMethodDefs[i], NULL, CORPROF_E_DATAINCOMPLETE); |
522 | continue; |
523 | } |
524 | |
525 | if (pModule->IsReflection()) |
526 | { |
527 | ReportReJITError(pModule, rgMethodDefs[i], NULL, CORPROF_E_MODULE_IS_DYNAMIC); |
528 | continue; |
529 | } |
530 | |
531 | if (!pModule->GetMDImport()->IsValidToken(rgMethodDefs[i])) |
532 | { |
533 | ReportReJITError(pModule, rgMethodDefs[i], NULL, E_INVALIDARG); |
534 | continue; |
535 | } |
536 | |
537 | MethodDesc * pMD = pModule->LookupMethodDef(rgMethodDefs[i]); |
538 | |
539 | if (pMD != NULL) |
540 | { |
541 | _ASSERTE(!pMD->IsNoMetadata()); |
542 | |
543 | // Weird, non-user functions can't be rejitted |
544 | if (!pMD->IsIL()) |
545 | { |
546 | // Intentionally not reporting an error in this case, to be consistent |
547 | // with the pre-rejit case, as we have no opportunity to report an error |
548 | // in a pre-rejit request for a non-IL method, since the rejit manager |
549 | // never gets a call from the prestub worker for non-IL methods. Thus, |
550 | // since pre-rejit requests silently ignore rejit requests for non-IL |
551 | // methods, regular rejit requests will also silently ignore rejit requests for |
552 | // non-IL methods to be consistent. |
553 | continue; |
554 | } |
555 | } |
556 | |
557 | CodeVersionManager * pCodeVersionManager = pModule->GetCodeVersionManager(); |
558 | _ASSERTE(pCodeVersionManager != NULL); |
559 | CodeActivationBatch * pCodeActivationBatch = mgrToCodeActivationBatch.Lookup(pCodeVersionManager); |
560 | if (pCodeActivationBatch == NULL) |
561 | { |
562 | pCodeActivationBatch = new (nothrow)CodeActivationBatch(pCodeVersionManager); |
563 | if (pCodeActivationBatch == NULL) |
564 | { |
565 | return E_OUTOFMEMORY; |
566 | } |
567 | |
568 | hr = S_OK; |
569 | EX_TRY |
570 | { |
571 | // This guy throws when out of memory, but remains internally |
572 | // consistent (without adding the new element) |
573 | mgrToCodeActivationBatch.Add(pCodeActivationBatch); |
574 | } |
575 | EX_CATCH_HRESULT(hr); |
576 | |
577 | _ASSERT(hr == S_OK || hr == E_OUTOFMEMORY); |
578 | if (FAILED(hr)) |
579 | { |
580 | return hr; |
581 | } |
582 | } |
583 | |
584 | { |
585 | CodeVersionManager::TableLockHolder lock(pCodeVersionManager); |
586 | |
587 | // Bind the il code version |
588 | ILCodeVersion* pILCodeVersion = pCodeActivationBatch->m_methodsToActivate.Append(); |
589 | if (pILCodeVersion == NULL) |
590 | { |
591 | return E_OUTOFMEMORY; |
592 | } |
593 | if (fIsRevert) |
594 | { |
595 | // activate the original version |
596 | *pILCodeVersion = ILCodeVersion(pModule, rgMethodDefs[i]); |
597 | } |
598 | else |
599 | { |
600 | // activate an unused or new IL version |
601 | hr = ReJitManager::BindILVersion(pCodeVersionManager, pModule, rgMethodDefs[i], pILCodeVersion); |
602 | if (FAILED(hr)) |
603 | { |
604 | _ASSERTE(hr == E_OUTOFMEMORY); |
605 | return hr; |
606 | } |
607 | } |
608 | } |
609 | } // for (ULONG i = 0; i < cFunctions; i++) |
610 | |
611 | // For each code versioning mgr, if there's work to do, suspend EE if needed, |
612 | // enter the code versioning mgr's crst, and do the batched work. |
613 | BOOL fEESuspended = FALSE; |
614 | SHash<CodeActivationBatchTraits>::Iterator beginIter = mgrToCodeActivationBatch.Begin(); |
615 | SHash<CodeActivationBatchTraits>::Iterator endIter = mgrToCodeActivationBatch.End(); |
616 | for (SHash<CodeActivationBatchTraits>::Iterator iter = beginIter; iter != endIter; iter++) |
617 | { |
618 | CodeActivationBatch * pCodeActivationBatch = *iter; |
619 | CodeVersionManager * pCodeVersionManager = pCodeActivationBatch->m_pCodeVersionManager; |
620 | |
621 | int cMethodsToActivate = pCodeActivationBatch->m_methodsToActivate.Count(); |
622 | if (cMethodsToActivate == 0) |
623 | { |
624 | continue; |
625 | } |
626 | |
627 | { |
628 | // SetActiveILCodeVersions takes the SystemDomain crst, which needs to be acquired before the |
629 | // ThreadStore crsts |
630 | SystemDomain::LockHolder lh; |
631 | |
632 | if(!fEESuspended) |
633 | { |
634 | // As a potential future optimization we could speculatively try to update the jump stamps without |
635 | // suspending the runtime. That needs to be plumbed through BatchUpdateJumpStamps though. |
636 | ThreadSuspend::SuspendEE(ThreadSuspend::SUSPEND_FOR_REJIT); |
637 | fEESuspended = TRUE; |
638 | } |
639 | |
640 | _ASSERTE(ThreadStore::HoldingThreadStore()); |
641 | hr = pCodeVersionManager->SetActiveILCodeVersions(pCodeActivationBatch->m_methodsToActivate.Ptr(), pCodeActivationBatch->m_methodsToActivate.Count(), fEESuspended, &errorRecords); |
642 | if (FAILED(hr)) |
643 | break; |
644 | } |
645 | } |
646 | if (fEESuspended) |
647 | { |
648 | ThreadSuspend::RestartEE(FALSE, TRUE); |
649 | } |
650 | |
651 | if (FAILED(hr)) |
652 | { |
653 | _ASSERTE(hr == E_OUTOFMEMORY); |
654 | return hr; |
655 | } |
656 | |
657 | // Report any errors that were batched up |
658 | for (int i = 0; i < errorRecords.Count(); i++) |
659 | { |
660 | if (rgHrStatuses != NULL) |
661 | { |
662 | for (DWORD j = 0; j < cFunctions; j++) |
663 | { |
664 | if (rgMethodDefs[j] == errorRecords[i].methodDef && |
665 | reinterpret_cast<Module*>(rgModuleIDs[j]) == errorRecords[i].pModule) |
666 | { |
667 | rgHrStatuses[j] = errorRecords[i].hrStatus; |
668 | } |
669 | } |
670 | } |
671 | else |
672 | { |
673 | ReportReJITError(&(errorRecords[i])); |
674 | } |
675 | |
676 | } |
677 | |
678 | // We got through processing everything, but profiler will need to see the individual ReJITError |
679 | // callbacks to know what, if anything, failed. |
680 | return S_OK; |
681 | } |
682 | |
683 | // static |
684 | HRESULT ReJitManager::BindILVersion( |
685 | CodeVersionManager* pCodeVersionManager, |
686 | PTR_Module pModule, |
687 | mdMethodDef methodDef, |
688 | ILCodeVersion *pILCodeVersion) |
689 | { |
690 | CONTRACTL |
691 | { |
692 | NOTHROW; |
693 | GC_NOTRIGGER; |
694 | MODE_PREEMPTIVE; |
695 | CAN_TAKE_LOCK; |
696 | PRECONDITION(CheckPointer(pCodeVersionManager)); |
697 | PRECONDITION(CheckPointer(pModule)); |
698 | PRECONDITION(CheckPointer(pILCodeVersion)); |
699 | } |
700 | CONTRACTL_END; |
701 | |
702 | _ASSERTE(pCodeVersionManager->LockOwnedByCurrentThread()); |
703 | _ASSERTE((pModule != NULL) && (methodDef != mdTokenNil)); |
704 | |
705 | // Check if there was there a previous rejit request for this method that hasn't been exposed back |
706 | // to the profiler yet |
707 | ILCodeVersion ilCodeVersion = pCodeVersionManager->GetActiveILCodeVersion(pModule, methodDef); |
708 | |
709 | if (ilCodeVersion.GetRejitState() == ILCodeVersion::kStateRequested) |
710 | { |
711 | // We can 'reuse' this instance because the profiler doesn't know about |
712 | // it yet. (This likely happened because a profiler called RequestReJIT |
713 | // twice in a row, without us having a chance to jmp-stamp the code yet OR |
714 | // while iterating through instantiations of a generic, the iterator found |
715 | // duplicate entries for the same instantiation.) |
716 | _ASSERTE(ilCodeVersion.HasDefaultIL()); |
717 | |
718 | *pILCodeVersion = ilCodeVersion; |
719 | return S_FALSE; |
720 | } |
721 | |
722 | // Either there was no ILCodeVersion yet for this MethodDesc OR whatever we've found |
723 | // couldn't be reused (and needed to be reverted). Create a new ILCodeVersion to return |
724 | // to the caller. |
725 | return pCodeVersionManager->AddILCodeVersion(pModule, methodDef, InterlockedIncrement(reinterpret_cast<LONG*>(&s_GlobalReJitId)), pILCodeVersion); |
726 | } |
727 | |
728 | //--------------------------------------------------------------------------------------- |
729 | // |
730 | // ICorProfilerInfo4::RequestRevert calls into this guy to do most of the |
731 | // work. Takes care of finding the appropriate ReJitManager instances to |
732 | // perform the revert |
733 | // |
734 | // Arguments: |
735 | // * cFunctions - Element count of rgModuleIDs & rgMethodDefs |
736 | // * rgModuleIDs - Parallel array of ModuleIDs to revert |
737 | // * rgMethodDefs - Parallel array of methodDefs to revert |
738 | // * rgHrStatuses - [out] Parallel array of HRESULTs indicating success/failure |
739 | // of reverting each (ModuleID, methodDef). |
740 | // |
741 | // Return Value: |
742 | // HRESULT indicating success or failure of the overall operation. Each |
743 | // individual methodDef (or MethodDesc associated with the methodDef) |
744 | // may encounter its own failure, which is reported by the rgHrStatuses |
745 | // [out] parameter. |
746 | // |
747 | |
748 | // static |
749 | HRESULT ReJitManager::RequestRevert( |
750 | ULONG cFunctions, |
751 | ModuleID rgModuleIDs[], |
752 | mdMethodDef rgMethodDefs[], |
753 | HRESULT rgHrStatuses[]) |
754 | { |
755 | CONTRACTL |
756 | { |
757 | NOTHROW; |
758 | GC_TRIGGERS; |
759 | CAN_TAKE_LOCK; |
760 | MODE_PREEMPTIVE; |
761 | } |
762 | CONTRACTL_END; |
763 | |
764 | return UpdateActiveILVersions(cFunctions, rgModuleIDs, rgMethodDefs, rgHrStatuses, TRUE); |
765 | } |
766 | |
767 | // static |
768 | HRESULT ReJitManager::ConfigureILCodeVersion(ILCodeVersion ilCodeVersion) |
769 | { |
770 | STANDARD_VM_CONTRACT; |
771 | |
772 | CodeVersionManager* pCodeVersionManager = ilCodeVersion.GetModule()->GetCodeVersionManager(); |
773 | _ASSERTE(!pCodeVersionManager->LockOwnedByCurrentThread()); |
774 | |
775 | |
776 | HRESULT hr = S_OK; |
777 | Module* pModule = ilCodeVersion.GetModule(); |
778 | mdMethodDef methodDef = ilCodeVersion.GetMethodDef(); |
779 | BOOL fNeedsParameters = FALSE; |
780 | BOOL fWaitForParameters = FALSE; |
781 | |
782 | { |
783 | // Serialize access to the rejit state |
784 | CodeVersionManager::TableLockHolder lock(pCodeVersionManager); |
785 | switch (ilCodeVersion.GetRejitState()) |
786 | { |
787 | case ILCodeVersion::kStateRequested: |
788 | ilCodeVersion.SetRejitState(ILCodeVersion::kStateGettingReJITParameters); |
789 | fNeedsParameters = TRUE; |
790 | break; |
791 | |
792 | case ILCodeVersion::kStateGettingReJITParameters: |
793 | fWaitForParameters = TRUE; |
794 | break; |
795 | |
796 | default: |
797 | return S_OK; |
798 | } |
799 | } |
800 | |
801 | if (fNeedsParameters) |
802 | { |
803 | // Here's where we give a chance for the rejit requestor to |
804 | // examine and modify the IL & codegen flags before it gets to |
805 | // the JIT. This allows one to add probe calls for things like |
806 | // code coverage, performance, or whatever. These will be |
807 | // stored in pShared. |
808 | _ASSERTE(pModule != NULL); |
809 | _ASSERTE(methodDef != mdTokenNil); |
810 | ReleaseHolder<ProfilerFunctionControl> pFuncControl = |
811 | new (nothrow)ProfilerFunctionControl(pModule->GetLoaderAllocator()->GetLowFrequencyHeap()); |
812 | HRESULT hr = S_OK; |
813 | if (pFuncControl == NULL) |
814 | { |
815 | hr = E_OUTOFMEMORY; |
816 | } |
817 | else |
818 | { |
819 | BEGIN_PIN_PROFILER(CORProfilerPresent()); |
820 | hr = g_profControlBlock.pProfInterface->GetReJITParameters( |
821 | (ModuleID)pModule, |
822 | methodDef, |
823 | pFuncControl); |
824 | END_PIN_PROFILER(); |
825 | } |
826 | |
827 | if (FAILED(hr)) |
828 | { |
829 | { |
830 | // Historically on failure we would revert to the kRequested state and fall-back |
831 | // to the initial code gen. The next time the method ran it would try again. |
832 | // |
833 | // Preserving that behavior is possible, but a bit awkward now that we have |
834 | // Precode swapping as well. Instead of doing that I am acting as if GetReJITParameters |
835 | // had succeeded, using the original IL, no jit flags, and no modified IL mapping. |
836 | // This is similar to a fallback except the profiler won't get any further attempts |
837 | // to provide the parameters correctly. If the profiler wants another attempt it would |
838 | // need to call RequestRejit again. |
839 | CodeVersionManager::TableLockHolder lock(pCodeVersionManager); |
840 | if (ilCodeVersion.GetRejitState() == ILCodeVersion::kStateGettingReJITParameters) |
841 | { |
842 | ilCodeVersion.SetRejitState(ILCodeVersion::kStateActive); |
843 | ilCodeVersion.SetIL(ILCodeVersion(pModule, methodDef).GetIL()); |
844 | } |
845 | } |
846 | ReportReJITError(pModule, methodDef, pModule->LookupMethodDef(methodDef), hr); |
847 | return S_OK; |
848 | } |
849 | else |
850 | { |
851 | CodeVersionManager::TableLockHolder lock(pCodeVersionManager); |
852 | if (ilCodeVersion.GetRejitState() == ILCodeVersion::kStateGettingReJITParameters) |
853 | { |
854 | // Inside the above call to ICorProfilerCallback4::GetReJITParameters, the profiler |
855 | // will have used the specified pFuncControl to provide its IL and codegen flags. |
856 | // So now we transfer it out to the SharedReJitInfo. |
857 | ilCodeVersion.SetJitFlags(pFuncControl->GetCodegenFlags()); |
858 | ilCodeVersion.SetIL((COR_ILMETHOD*)pFuncControl->GetIL()); |
859 | // ilCodeVersion is now the owner of the memory for the IL buffer |
860 | ilCodeVersion.SetInstrumentedILMap(pFuncControl->GetInstrumentedMapEntryCount(), |
861 | pFuncControl->GetInstrumentedMapEntries()); |
862 | ilCodeVersion.SetRejitState(ILCodeVersion::kStateActive); |
863 | } |
864 | } |
865 | } |
866 | else if (fWaitForParameters) |
867 | { |
868 | // This feels lame, but it doesn't appear like we have the good threading primitves |
869 | // for this. What I would like is an AutoResetEvent that atomically exits the table |
870 | // Crst when I wait on it. From what I can tell our AutoResetEvent doesn't have |
871 | // that atomic transition which means this ordering could occur: |
872 | // [Thread 1] detect kStateGettingParameters and exit table lock |
873 | // [Thread 2] enter table lock, transition kStateGettingParameters -> kStateActive |
874 | // [Thread 2] signal AutoResetEvent |
875 | // [Thread 2] exit table lock |
876 | // [Thread 1] wait on AutoResetEvent (which may never be signaled again) |
877 | // |
878 | // Another option would be ManualResetEvents, one for each SharedReJitInfo, but |
879 | // that feels like a lot of memory overhead to handle a case which occurs rarely. |
880 | // A third option would be dynamically creating ManualResetEvents in a side |
881 | // dictionary on demand, but that feels like a lot of complexity for an event |
882 | // that occurs rarely. |
883 | // |
884 | // I just ended up with this simple polling loop. Assuming profiler |
885 | // writers implement GetReJITParameters performantly we will only iterate |
886 | // this loop once, and even then only in the rare case of threads racing |
887 | // to JIT the same IL. If this really winds up causing performance issues |
888 | // We can build something more sophisticated. |
889 | while (true) |
890 | { |
891 | { |
892 | CodeVersionManager::TableLockHolder lock(pCodeVersionManager); |
893 | if (ilCodeVersion.GetRejitState() == ILCodeVersion::kStateActive) |
894 | { |
895 | break; // the other thread got the parameters succesfully, go race to rejit |
896 | } |
897 | } |
898 | ClrSleepEx(1, FALSE); |
899 | } |
900 | } |
901 | |
902 | return S_OK; |
903 | } |
904 | |
905 | #endif // DACCESS_COMPILE |
906 | // The rest of the ReJitManager methods are safe to compile for DAC |
907 | |
908 | //--------------------------------------------------------------------------------------- |
909 | // |
910 | // Used by profiler to get the ReJITID corrseponding to a (MethodDesc *, PCODE) pair. |
911 | // Can also be used to determine whether (MethodDesc *, PCODE) corresponds to a rejit |
912 | // (vs. a regular JIT) for the purposes of deciding whether to notify the debugger about |
913 | // the rejit (and building the debugger JIT info structure). |
914 | // |
915 | // Arguments: |
916 | // * pMD - MethodDesc * of interestg |
917 | // * pCodeStart - PCODE of the particular interesting JITting of that MethodDesc * |
918 | // |
919 | // Return Value: |
920 | // 0 if no such ReJITID found (e.g., PCODE is from a JIT and not a rejit), else the |
921 | // ReJITID requested. |
922 | // |
923 | // static |
924 | ReJITID ReJitManager::GetReJitId(PTR_MethodDesc pMD, PCODE pCodeStart) |
925 | { |
926 | CONTRACTL |
927 | { |
928 | NOTHROW; |
929 | CAN_TAKE_LOCK; |
930 | GC_TRIGGERS; |
931 | PRECONDITION(CheckPointer(pMD)); |
932 | PRECONDITION(pCodeStart != NULL); |
933 | } |
934 | CONTRACTL_END; |
935 | |
936 | // Fast-path: If the rejit map is empty, no need to look up anything. Do this outside |
937 | // of a lock to impact our caller (the prestub worker) as little as possible. If the |
938 | // map is nonempty, we'll acquire the lock at that point and do the lookup for real. |
939 | CodeVersionManager* pCodeVersionManager = pMD->GetCodeVersionManager(); |
940 | if (pCodeVersionManager->GetNonDefaultILVersionCount() == 0) |
941 | { |
942 | return 0; |
943 | } |
944 | |
945 | CodeVersionManager::TableLockHolder ch(pCodeVersionManager); |
946 | return ReJitManager::GetReJitIdNoLock(pMD, pCodeStart); |
947 | } |
948 | |
949 | //--------------------------------------------------------------------------------------- |
950 | // |
951 | // See comment above code:ReJitManager::GetReJitId for main details of what this does. |
952 | // |
953 | // This function is basically the same as GetReJitId, except caller is expected to take |
954 | // the ReJitManager lock directly (via ReJitManager::TableLockHolder). This exists so |
955 | // that ETW can explicitly take the triggering ReJitManager lock up front, and in the |
956 | // proper order, to avoid lock leveling issues, and triggering issues with other locks it |
957 | // takes that are CRST_UNSAFE_ANYMODE |
958 | // |
959 | |
960 | ReJITID ReJitManager::GetReJitIdNoLock(PTR_MethodDesc pMD, PCODE pCodeStart) |
961 | { |
962 | CONTRACTL |
963 | { |
964 | NOTHROW; |
965 | CANNOT_TAKE_LOCK; |
966 | GC_NOTRIGGER; |
967 | PRECONDITION(CheckPointer(pMD)); |
968 | PRECONDITION(pCodeStart != NULL); |
969 | } |
970 | CONTRACTL_END; |
971 | |
972 | // Caller must ensure this lock is taken! |
973 | CodeVersionManager* pCodeVersionManager = pMD->GetCodeVersionManager(); |
974 | _ASSERTE(pCodeVersionManager->LockOwnedByCurrentThread()); |
975 | |
976 | NativeCodeVersion nativeCodeVersion = pCodeVersionManager->GetNativeCodeVersion(pMD, pCodeStart); |
977 | if (nativeCodeVersion.IsNull()) |
978 | { |
979 | return 0; |
980 | } |
981 | return nativeCodeVersion.GetILCodeVersion().GetVersionId(); |
982 | } |
983 | |
984 | //--------------------------------------------------------------------------------------- |
985 | // |
986 | // Called by profiler to retrieve an array of ReJITIDs corresponding to a MethodDesc * |
987 | // |
988 | // Arguments: |
989 | // * pMD - MethodDesc * to look up |
990 | // * cReJitIds - Element count capacity of reJitIds |
991 | // * pcReJitIds - [out] Place total count of ReJITIDs found here; may be more than |
992 | // cReJitIds if profiler passed an array that's too small to hold them all |
993 | // * reJitIds - [out] Place ReJITIDs found here. Count of ReJITIDs returned here is |
994 | // min(cReJitIds, *pcReJitIds) |
995 | // |
996 | // Return Value: |
997 | // * S_OK: ReJITIDs successfully returned, array is big enough |
998 | // * S_FALSE: ReJITIDs successfully found, but array was not big enough. Only |
999 | // cReJitIds were returned and cReJitIds < *pcReJitId (latter being the total |
1000 | // number of ReJITIDs available). |
1001 | // |
1002 | // static |
1003 | HRESULT ReJitManager::GetReJITIDs(PTR_MethodDesc pMD, ULONG cReJitIds, ULONG * pcReJitIds, ReJITID reJitIds[]) |
1004 | { |
1005 | CONTRACTL |
1006 | { |
1007 | NOTHROW; |
1008 | CAN_TAKE_LOCK; |
1009 | GC_NOTRIGGER; |
1010 | PRECONDITION(CheckPointer(pMD)); |
1011 | PRECONDITION(pcReJitIds != NULL); |
1012 | PRECONDITION(reJitIds != NULL); |
1013 | } |
1014 | CONTRACTL_END; |
1015 | |
1016 | CodeVersionManager* pCodeVersionManager = pMD->GetCodeVersionManager(); |
1017 | CodeVersionManager::TableLockHolder lock(pCodeVersionManager); |
1018 | |
1019 | ULONG cnt = 0; |
1020 | |
1021 | ILCodeVersionCollection ilCodeVersions = pCodeVersionManager->GetILCodeVersions(pMD); |
1022 | for (ILCodeVersionIterator iter = ilCodeVersions.Begin(), end = ilCodeVersions.End(); |
1023 | iter != end; |
1024 | iter++) |
1025 | { |
1026 | ILCodeVersion curILVersion = *iter; |
1027 | |
1028 | if (curILVersion.GetRejitState() == ILCodeVersion::kStateActive) |
1029 | { |
1030 | if (cnt < cReJitIds) |
1031 | { |
1032 | reJitIds[cnt] = curILVersion.GetVersionId(); |
1033 | } |
1034 | ++cnt; |
1035 | |
1036 | // no overflow |
1037 | _ASSERTE(cnt != 0); |
1038 | } |
1039 | } |
1040 | *pcReJitIds = cnt; |
1041 | |
1042 | return (cnt > cReJitIds) ? S_FALSE : S_OK; |
1043 | } |
1044 | |
1045 | #endif // FEATURE_CODE_VERSIONING |
1046 | #else // FEATURE_REJIT |
1047 | |
1048 | // On architectures that don't support rejit, just keep around some do-nothing |
1049 | // stubs so the rest of the VM doesn't have to be littered with #ifdef FEATURE_REJIT |
1050 | |
1051 | // static |
1052 | HRESULT ReJitManager::RequestReJIT( |
1053 | ULONG cFunctions, |
1054 | ModuleID rgModuleIDs[], |
1055 | mdMethodDef rgMethodDefs[]) |
1056 | { |
1057 | return E_NOTIMPL; |
1058 | } |
1059 | |
1060 | // static |
1061 | HRESULT ReJitManager::RequestRevert( |
1062 | ULONG cFunctions, |
1063 | ModuleID rgModuleIDs[], |
1064 | mdMethodDef rgMethodDefs[], |
1065 | HRESULT rgHrStatuses[]) |
1066 | { |
1067 | return E_NOTIMPL; |
1068 | } |
1069 | |
1070 | ReJITID ReJitManager::GetReJitId(PTR_MethodDesc pMD, PCODE pCodeStart) |
1071 | { |
1072 | return 0; |
1073 | } |
1074 | |
1075 | ReJITID ReJitManager::GetReJitIdNoLock(PTR_MethodDesc pMD, PCODE pCodeStart) |
1076 | { |
1077 | return 0; |
1078 | } |
1079 | |
1080 | HRESULT ReJitManager::GetReJITIDs(PTR_MethodDesc pMD, ULONG cReJitIds, ULONG * pcReJitIds, ReJITID reJitIds[]) |
1081 | { |
1082 | return E_NOTIMPL; |
1083 | } |
1084 | |
1085 | #endif // FEATURE_REJIT |
1086 | |