Merge pull request #11275 from CarolEidt/Fix11141
[platform/upstream/coreclr.git] / src / vm / tieredcompilation.cpp
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 // File: TieredCompilation.CPP
6 //
7 // ===========================================================================
8
9
10
11 #include "common.h"
12 #include "excep.h"
13 #include "log.h"
14 #include "win32threadpool.h"
15 #include "tieredcompilation.h"
16
17 // TieredCompilationManager determines which methods should be recompiled and
18 // how they should be recompiled to best optimize the running code. It then
19 // handles logistics of getting new code created and installed.
20 //
21 //
22 // # Current feature state
23 //
24 // This feature is incomplete and currently experimental. To enable it
25 // you need to set COMPLUS_EXPERIMENTAL_TieredCompilation = 1. When the environment
26 // variable is unset the runtime should work as normal, but when it is set there are 
27 // anticipated incompatibilities and limited cross cutting test coverage so far.
28 //   Profiler - Anticipated incompatible with ReJIT, untested in general
29 //   ETW - Anticipated incompatible with the ReJIT id of the MethodJitted rundown events
30 //   Managed debugging - Anticipated incompatible with breakpoints/stepping that are
31 //                       active when a method is recompiled.
32 //   
33 //
34 // Testing that has been done so far largely consists of regression testing with
35 // the environment variable off + functional/perf testing of the Music Store ASP.Net
36 // workload as a basic example that the feature can work. Running the coreclr repo
37 // tests with the env var on generates about a dozen failures in JIT tests. The issues
38 // are likely related to assertions about optimization behavior but haven't been
39 // properly investigated yet.
40 //
41 // If you decide to try this out on a new workload and run into trouble a quick note
42 // on github is appreciated but this code may have high churn for a while to come and
43 // there will be no sense investing a lot of time investigating only to have it rendered 
44 // moot by changes. I aim to keep this comment updated as things change.
45 //
46 //
47 // # Important entrypoints in this code:
48 //
49 // 
50 // a) .ctor and Init(...) - called once during AppDomain initialization
51 // b) OnMethodCalled(...) - called when a method is being invoked. When a method
52 //                     has been called enough times this is currently the only
53 //                     trigger that initiates re-compilation.
54 // c) OnAppDomainShutdown() - called during AppDomain::Exit() to begin the process
55 //                     of stopping tiered compilation. After this point no more
56 //                     background optimization work will be initiated but in-progress
57 //                     work still needs to complete.
58 //
59 // # Overall workflow
60 //
61 // Methods initially call into OnMethodCalled() and once the call count exceeds
62 // a fixed limit we queue work on to our internal list of methods needing to
63 // be recompiled (m_methodsToOptimize). If there is currently no thread
64 // servicing our queue asynchronously then we use the runtime threadpool
65 // QueueUserWorkItem to recruit one. During the callback for each threadpool work
66 // item we handle as many methods as possible in a fixed period of time, then
67 // queue another threadpool work item if m_methodsToOptimize hasn't been drained.
68 //
69 // The background thread enters at StaticOptimizeMethodsCallback(), enters the
70 // appdomain, and then begins calling OptimizeMethod on each method in the
71 // queue. For each method we jit it, then update the precode so that future
72 // entrypoint callers will run the new code.
73 // 
74 // # Error handling
75 //
76 // The overall principle is don't swallow terminal failures that may have corrupted the
77 // process (AV for example), but otherwise for any transient issue or functional limitation
78 // that prevents us from optimizing log it for diagnostics and then back out gracefully,
79 // continuing to run the less optimal code. The feature should be constructed so that
80 // errors are limited to OS resource exhaustion or poorly behaved managed code
81 // (for example within an AssemblyResolve event or static constructor triggered by the JIT).
82
83 #ifdef FEATURE_TIERED_COMPILATION
84
85 // Called at AppDomain construction
86 TieredCompilationManager::TieredCompilationManager() :
87     m_isAppDomainShuttingDown(FALSE),
88     m_countOptimizationThreadsRunning(0),
89     m_callCountOptimizationThreshhold(30),
90     m_optimizationQuantumMs(50)
91 {
92     LIMITED_METHOD_CONTRACT;
93     m_lock.Init(LOCK_TYPE_DEFAULT);
94 }
95
96 // Called at AppDomain Init
97 void TieredCompilationManager::Init(ADID appDomainId)
98 {
99     CONTRACTL
100     {
101         NOTHROW;
102         GC_NOTRIGGER;
103         CAN_TAKE_LOCK;
104         MODE_PREEMPTIVE;
105     }
106     CONTRACTL_END;
107
108     SpinLockHolder holder(&m_lock);
109     m_domainId = appDomainId;
110 }
111
112 // Called each time code in this AppDomain has been run. This is our sole entrypoint to begin
113 // tiered compilation for now. Returns TRUE if no more notifications are necessary, but
114 // more notifications may come anyways.
115 //
116 // currentCallCount is pre-incremented, that is to say the value is 1 on first call for a given
117 //      method.
118 BOOL TieredCompilationManager::OnMethodCalled(MethodDesc* pMethodDesc, DWORD currentCallCount)
119 {
120     STANDARD_VM_CONTRACT;
121
122     if (currentCallCount < m_callCountOptimizationThreshhold)
123     {
124         return FALSE; // continue notifications for this method
125     }
126     else if (currentCallCount > m_callCountOptimizationThreshhold)
127     {
128         return TRUE; // stop notifications for this method
129     }
130
131     // Insert the method into the optimization queue and trigger a thread to service
132     // the queue if needed.
133     //
134     // Terminal exceptions escape as exceptions, but all other errors should gracefully
135     // return to the caller. Non-terminal error conditions should be rare (ie OOM,
136     // OS failure to create thread) and we consider it reasonable for some methods
137     // to go unoptimized or have their optimization arbitrarily delayed under these
138     // circumstances. Note an error here could affect concurrent threads running this
139     // code. Those threads will observe m_countOptimizationThreadsRunning > 0 and return,
140     // then QueueUserWorkItem fails on this thread lowering the count and leaves them 
141     // unserviced. Synchronous retries appear unlikely to offer any material improvement 
142     // and complicating the code to narrow an already rare error case isn't desirable.
143     {
144         SListElem<MethodDesc*>* pMethodListItem = new (nothrow) SListElem<MethodDesc*>(pMethodDesc);
145         SpinLockHolder holder(&m_lock);
146         if (pMethodListItem != NULL)
147         {
148             m_methodsToOptimize.InsertTail(pMethodListItem);
149         }
150
151         if (0 == m_countOptimizationThreadsRunning && !m_isAppDomainShuttingDown)
152         {
153             // Our current policy throttles at 1 thread, but in the future we
154             // could experiment with more parallelism.
155             m_countOptimizationThreadsRunning++;
156         }
157         else
158         {
159             return TRUE; // stop notifications for this method
160         }
161     }
162
163     EX_TRY
164     {
165         if (!ThreadpoolMgr::QueueUserWorkItem(StaticOptimizeMethodsCallback, this, QUEUE_ONLY, TRUE))
166         {
167             SpinLockHolder holder(&m_lock);
168             m_countOptimizationThreadsRunning--;
169             STRESS_LOG1(LF_TIEREDCOMPILATION, LL_WARNING, "TieredCompilationManager::OnMethodCalled: "
170                 "ThreadpoolMgr::QueueUserWorkItem returned FALSE (no thread will run), method=%pM\n",
171                 pMethodDesc);
172         }
173     }
174     EX_CATCH
175     {
176         SpinLockHolder holder(&m_lock);
177         m_countOptimizationThreadsRunning--;
178         STRESS_LOG2(LF_TIEREDCOMPILATION, LL_WARNING, "TieredCompilationManager::OnMethodCalled: "
179             "Exception queuing work item to threadpool, hr=0x%x, method=%pM\n",
180             GET_EXCEPTION()->GetHR(), pMethodDesc);
181     }
182     EX_END_CATCH(RethrowTerminalExceptions);
183
184     return TRUE; // stop notifications for this method
185 }
186
187 void TieredCompilationManager::OnAppDomainShutdown()
188 {
189     SpinLockHolder holder(&m_lock);
190     m_isAppDomainShuttingDown = TRUE;
191 }
192
193 // This is the initial entrypoint for the background thread, called by
194 // the threadpool.
195 DWORD WINAPI TieredCompilationManager::StaticOptimizeMethodsCallback(void *args)
196 {
197     STANDARD_VM_CONTRACT;
198
199     TieredCompilationManager * pTieredCompilationManager = (TieredCompilationManager *)args;
200     pTieredCompilationManager->OptimizeMethodsCallback();
201
202     return 0;
203 }
204
205 //This method will process one or more methods from optimization queue
206 // on a background thread. Each such method will be jitted with code
207 // optimizations enabled and then installed as the active implementation
208 // of the method entrypoint.
209 // 
210 // We need to be carefuly not to work for too long in a single invocation
211 // of this method or we could starve the threadpool and force
212 // it to create unnecessary additional threads.
213 void TieredCompilationManager::OptimizeMethodsCallback()
214 {
215     STANDARD_VM_CONTRACT;
216
217     // This app domain shutdown check isn't required for correctness
218     // but it should reduce some unneeded exceptions trying
219     // to enter a closed AppDomain
220     {
221         SpinLockHolder holder(&m_lock);
222         if (m_isAppDomainShuttingDown)
223         {
224             m_countOptimizationThreadsRunning--;
225             return;
226         }
227     }
228
229     ULONGLONG startTickCount = CLRGetTickCount64();
230     MethodDesc* pMethod = NULL;
231     EX_TRY
232     {
233         ENTER_DOMAIN_ID(m_domainId);
234         {
235             while (true)
236             {
237                 {
238                     SpinLockHolder holder(&m_lock); 
239                     pMethod = GetNextMethodToOptimize();
240                     if (pMethod == NULL ||
241                         m_isAppDomainShuttingDown)
242                     {
243                         m_countOptimizationThreadsRunning--;
244                         break;
245                     }
246                     
247                 }
248                 OptimizeMethod(pMethod);
249
250                 // If we have been running for too long return the thread to the threadpool and queue another event
251                 // This gives the threadpool a chance to service other requests on this thread before returning to
252                 // this work.
253                 ULONGLONG currentTickCount = CLRGetTickCount64();
254                 if (currentTickCount >= startTickCount + m_optimizationQuantumMs)
255                 {
256                     if (!ThreadpoolMgr::QueueUserWorkItem(StaticOptimizeMethodsCallback, this, QUEUE_ONLY, TRUE))
257                     {
258                         SpinLockHolder holder(&m_lock);
259                         m_countOptimizationThreadsRunning--;
260                         STRESS_LOG0(LF_TIEREDCOMPILATION, LL_WARNING, "TieredCompilationManager::OptimizeMethodsCallback: "
261                             "ThreadpoolMgr::QueueUserWorkItem returned FALSE (no thread will run)\n");
262                     }
263                     break;
264                 }
265             }
266         }
267         END_DOMAIN_TRANSITION;
268     }
269     EX_CATCH
270     {
271         STRESS_LOG2(LF_TIEREDCOMPILATION, LL_ERROR, "TieredCompilationManager::OptimizeMethodsCallback: "
272             "Unhandled exception during method optimization, hr=0x%x, last method=%pM\n",
273             GET_EXCEPTION()->GetHR(), pMethod);
274     }
275     EX_END_CATCH(RethrowTerminalExceptions);
276 }
277
278 // Jit compiles and installs new optimized code for a method.
279 // Called on a background thread.
280 void TieredCompilationManager::OptimizeMethod(MethodDesc* pMethod)
281 {
282     STANDARD_VM_CONTRACT;
283
284     _ASSERTE(pMethod->IsEligibleForTieredCompilation());
285     PCODE pJittedCode = CompileMethod(pMethod);
286     if (pJittedCode != NULL)
287     {
288         InstallMethodCode(pMethod, pJittedCode);
289     }
290 }
291
292 // Compiles new optimized code for a method.
293 // Called on a background thread.
294 PCODE TieredCompilationManager::CompileMethod(MethodDesc* pMethod)
295 {
296     STANDARD_VM_CONTRACT;
297
298     PCODE pCode = NULL;
299     ULONG sizeOfCode = 0;
300     EX_TRY
301     {
302         CORJIT_FLAGS flags = CORJIT_FLAGS(CORJIT_FLAGS::CORJIT_FLAG_MCJIT_BACKGROUND);
303         flags.Add(CORJIT_FLAGS(CORJIT_FLAGS::CORJIT_FLAG_TIER1));
304
305         if (pMethod->IsDynamicMethod())
306         {
307             ILStubResolver* pResolver = pMethod->AsDynamicMethodDesc()->GetILStubResolver();
308             flags.Add(pResolver->GetJitFlags());
309             COR_ILMETHOD_DECODER* pILheader = pResolver->GetILHeader();
310             pCode = UnsafeJitFunction(pMethod, pILheader, flags, &sizeOfCode);
311         }
312         else
313         {
314             COR_ILMETHOD_DECODER::DecoderStatus status;
315             COR_ILMETHOD_DECODER header(pMethod->GetILHeader(), pMethod->GetModule()->GetMDImport(), &status);
316             pCode = UnsafeJitFunction(pMethod, &header, flags, &sizeOfCode);
317         }
318     }
319     EX_CATCH
320     {
321         // Failing to jit should be rare but acceptable. We will leave whatever code already exists in place.
322         STRESS_LOG2(LF_TIEREDCOMPILATION, LL_INFO10, "TieredCompilationManager::CompileMethod: Method %pM failed to jit, hr=0x%x\n", 
323             pMethod, GET_EXCEPTION()->GetHR());
324     }
325     EX_END_CATCH(RethrowTerminalExceptions)
326
327     return pCode;
328 }
329
330 // Updates the MethodDesc and precode so that future invocations of a method will
331 // execute the native code pointed to by pCode.
332 // Called on a background thread.
333 void TieredCompilationManager::InstallMethodCode(MethodDesc* pMethod, PCODE pCode)
334 {
335     STANDARD_VM_CONTRACT;
336
337     _ASSERTE(!pMethod->IsNativeCodeStableAfterInit());
338
339     PCODE pExistingCode = pMethod->GetNativeCode();
340 #ifdef FEATURE_INTERPRETER
341     if (!pMethod->SetNativeCodeInterlocked(pCode, pExistingCode, TRUE))
342 #else
343     if (!pMethod->SetNativeCodeInterlocked(pCode, pExistingCode))
344 #endif
345     {
346         //We aren't there yet, but when the feature is finished we shouldn't be racing against any other code mutator and there would be no
347         //reason for this to fail
348         STRESS_LOG2(LF_TIEREDCOMPILATION, LL_INFO10, "TieredCompilationManager::InstallMethodCode: Method %pM failed to update native code slot. Code=%pK\n",
349             pMethod, pCode);
350     }
351     else
352     {
353         Precode* pPrecode = pMethod->GetPrecode();
354         if (!pPrecode->SetTargetInterlocked(pCode, FALSE))
355         {
356             //We aren't there yet, but when the feature is finished we shouldn't be racing against any other code mutator and there would be no
357             //reason for this to fail
358             STRESS_LOG2(LF_TIEREDCOMPILATION, LL_INFO10, "TieredCompilationManager::InstallMethodCode: Method %pM failed to update precode. Code=%pK\n",
359                 pMethod, pCode);
360         }
361     }
362 }
363
364 // Dequeues the next method in the optmization queue.
365 // This should be called with m_lock already held and runs
366 // on the background thread.
367 MethodDesc* TieredCompilationManager::GetNextMethodToOptimize()
368 {
369     STANDARD_VM_CONTRACT;
370
371     SListElem<MethodDesc*>* pElem = m_methodsToOptimize.RemoveHead();
372     if (pElem != NULL)
373     {
374         MethodDesc* pMD = pElem->GetValue();
375         delete pElem;
376         return pMD;
377     }
378     return NULL;
379 }
380
381 #endif // FEATURE_TIERED_COMPILATION