• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2024 Google LLC
3  *
4  * Use of this source code is governed by a BSD-style license that can be
5  * found in the LICENSE file.
6  */
7 
8 #include "tests/Test.h"
9 
10 #if defined(SK_GRAPHITE)
11 
12 #include "include/core/SkCanvas.h"
13 #include "include/core/SkPaint.h"
14 #include "include/core/SkTextBlob.h"
15 #include "include/effects/SkGradientShader.h"
16 #include "include/gpu/graphite/PrecompileContext.h"
17 #include "include/gpu/graphite/Surface.h"
18 #include "include/gpu/graphite/precompile/PaintOptions.h"
19 #include "include/gpu/graphite/precompile/Precompile.h"
20 #include "include/gpu/graphite/precompile/PrecompileShader.h"
21 #include "src/gpu/graphite/ContextPriv.h"
22 #include "src/gpu/graphite/GraphicsPipelineDesc.h"
23 #include "src/gpu/graphite/RenderPassDesc.h"
24 #include "src/gpu/graphite/ResourceProvider.h"
25 #include "tools/fonts/FontToolUtils.h"
26 #include "tools/graphite/UniqueKeyUtils.h"
27 
28 #include <algorithm>
29 #include <random>
30 #include <thread>
31 
32 using namespace::skgpu::graphite;
33 
34 namespace {
35 
36 static constexpr int kMaxNumStops = 9;
37 static constexpr SkColor gColors[kMaxNumStops] = {
38         SK_ColorRED,
39         SK_ColorGREEN,
40         SK_ColorBLUE,
41         SK_ColorCYAN,
42         SK_ColorMAGENTA,
43         SK_ColorYELLOW,
44         SK_ColorBLACK,
45         SK_ColorDKGRAY,
46         SK_ColorLTGRAY,
47 };
48 static constexpr SkPoint gPts[kMaxNumStops] = {
49         { -100.0f, -100.0f },
50         { -50.0f, -50.0f },
51         { -25.0f, -25.0f },
52         { -12.5f, -12.5f },
53         { 0.0f, 0.0f },
54         { 12.5f, 12.5f },
55         { 25.0f, 25.0f },
56         { 50.0f, 50.0f },
57         { 100.0f, 100.0f }
58 };
59 static constexpr float gOffsets[kMaxNumStops] =
60             { 0.0f, 0.125f, 0.25f, 0.375f, 0.5f, 0.625f, 0.75f, 0.875f, 1.0f };
61 
linear(int numStops)62 std::pair<SkPaint, PaintOptions> linear(int numStops) {
63     SkASSERT(numStops <= kMaxNumStops);
64 
65     PaintOptions paintOptions;
66     paintOptions.setShaders({ PrecompileShaders::LinearGradient() });
67     paintOptions.setBlendModes({ SkBlendMode::kSrcOver });
68 
69     SkPaint paint;
70     paint.setShader(SkGradientShader::MakeLinear(gPts,
71                                                  gColors, gOffsets, numStops,
72                                                  SkTileMode::kClamp));
73     paint.setBlendMode(SkBlendMode::kSrcOver);
74 
75     return { paint, paintOptions };
76 }
77 
radial(int numStops)78 std::pair<SkPaint, PaintOptions> radial(int numStops) {
79     SkASSERT(numStops <= kMaxNumStops);
80 
81     PaintOptions paintOptions;
82     paintOptions.setShaders({ PrecompileShaders::RadialGradient() });
83     paintOptions.setBlendModes({ SkBlendMode::kSrcOver });
84 
85     SkPaint paint;
86     paint.setShader(SkGradientShader::MakeRadial(/* center= */ {0, 0}, /* radius= */ 100,
87                                                  gColors, gOffsets, numStops,
88                                                  SkTileMode::kClamp));
89     paint.setBlendMode(SkBlendMode::kSrcOver);
90 
91     return { paint, paintOptions };
92 }
93 
sweep(int numStops)94 std::pair<SkPaint, PaintOptions> sweep(int numStops) {
95     SkASSERT(numStops <= kMaxNumStops);
96 
97     PaintOptions paintOptions;
98     paintOptions.setShaders({ PrecompileShaders::SweepGradient() });
99     paintOptions.setBlendModes({ SkBlendMode::kSrcOver });
100 
101     SkPaint paint;
102     paint.setShader(SkGradientShader::MakeSweep(/* cx= */ 0, /* cy= */ 0,
103                                                 gColors, gOffsets, numStops,
104                                                 SkTileMode::kClamp,
105                                                 /* startAngle= */ 0, /* endAngle= */ 359,
106                                                 /* flags= */ 0, /* localMatrix= */ nullptr));
107     paint.setBlendMode(SkBlendMode::kSrcOver);
108 
109     return { paint, paintOptions };
110 }
111 
conical(int numStops)112 std::pair<SkPaint, PaintOptions> conical(int numStops) {
113     SkASSERT(numStops <= kMaxNumStops);
114 
115     PaintOptions paintOptions;
116     paintOptions.setShaders({ PrecompileShaders::TwoPointConicalGradient() });
117     paintOptions.setBlendModes({ SkBlendMode::kSrcOver });
118 
119     SkPaint paint;
120     paint.setShader(SkGradientShader::MakeTwoPointConical(/* start= */ {100, 100},
121                                                           /* startRadius= */ 100,
122                                                           /* end= */ {-100, -100},
123                                                           /* endRadius= */ 100,
124                                                           gColors, gOffsets, numStops,
125                                                           SkTileMode::kClamp));
126     paint.setBlendMode(SkBlendMode::kSrcOver);
127 
128     return { paint, paintOptions };
129 }
130 
131 // The 12 comes from 4 types of gradient times 3 combinations (i.e., 4,8,N) for each one.
132 static constexpr int kNumDiffPipelines = 12;
133 
134 typedef std::pair<SkPaint, PaintOptions> (*GradientCreationFunc)(int numStops);
135 
136 struct Combo {
137     GradientCreationFunc fCreateOptionsMtd;
138     int fNumStops;
139 };
140 
precompile_gradients(std::unique_ptr<PrecompileContext> precompileContext,bool permute,skiatest::Reporter *,int)141 void precompile_gradients(std::unique_ptr<PrecompileContext> precompileContext,
142                           bool permute,
143                           skiatest::Reporter* /* reporter */,
144                           int /* threadID */) {
145     std::array<Combo, 4> combos;
146 
147     // numStops doesn't influence the paintOptions
148     combos[0] = { linear,  /* fNumStops= */ 2 };
149     combos[1] = { radial,  /* fNumStops= */ 2 };
150     combos[2] = { sweep,   /* fNumStops= */ 2 };
151     combos[3] = { conical, /* fNumStops= */ 2 };
152 
153     if (permute) {
154         std::random_device rd;
155         std::mt19937 g(rd());
156 
157         std::shuffle(combos.begin(), combos.end(), g);
158     }
159 
160     const RenderPassProperties kProps = { DepthStencilFlags::kDepth,
161                                           kBGRA_8888_SkColorType,
162                                           /* dstColorSpace= */ nullptr,
163                                           /* requiresMSAA= */ false };
164 
165     for (auto c : combos) {
166         auto [_, paintOptions] = c.fCreateOptionsMtd(c.fNumStops);
167         Precompile(precompileContext.get(),
168                    paintOptions,
169                    DrawTypeFlags::kBitmapText_Mask,
170                    { &kProps, 1 });
171     }
172 
173     precompileContext.reset();
174 }
175 
purge_on_thread(std::unique_ptr<PrecompileContext> precompileContext,std::atomic_bool * keepLooping,skiatest::Reporter *,int)176 void purge_on_thread(std::unique_ptr<PrecompileContext> precompileContext,
177                      std::atomic_bool* keepLooping,
178                      skiatest::Reporter* /* reporter */,
179                      int /* threadID */) {
180     const auto kSleepDuration = std::chrono::milliseconds(1);
181 
182     while (*keepLooping) {
183         std::this_thread::sleep_for(kSleepDuration);
184 
185         precompileContext->purgePipelinesNotUsedInMs(kSleepDuration);
186     }
187 
188     precompileContext.reset();
189 }
190 
191 // A simple helper to call Context::insertRecording on the Recordings generated on the
192 // recorder threads. It collects (and keeps ownership) of all the generated Recordings.
193 class Listener : public SkRefCnt {
194 public:
Listener(int numSenders)195     Listener(int numSenders) : fNumActiveSenders(numSenders) {}
196 
addRecording(std::unique_ptr<Recording> recording)197     void addRecording(std::unique_ptr<Recording> recording) SK_EXCLUDES(fLock) {
198         {
199             SkAutoMutexExclusive lock(fLock);
200             fRecordings.push_back(std::move(recording));
201         }
202 
203         fWorkAvailable.signal(1);
204     }
205 
deregister()206     void deregister() SK_EXCLUDES(fLock) {
207         {
208             SkAutoMutexExclusive lock(fLock);
209             fNumActiveSenders--;
210         }
211 
212         fWorkAvailable.signal(1);
213     }
214 
insertRecordings(Context * context)215     void insertRecordings(Context* context) {
216         do {
217             fWorkAvailable.wait();
218         } while (this->insertRecording(context));
219     }
220 
221 private:
222     // This entry point is run in a loop waiting on the 'fWorkAvailable' semaphore until there
223     // are no senders remaining (at which point it returns false) c.f. 'insertRecordings'.
insertRecording(Context * context)224     bool insertRecording(Context* context) SK_EXCLUDES(fLock) {
225         Recording* recording = nullptr;
226         int numSendersLeft;
227 
228         {
229             SkAutoMutexExclusive lock(fLock);
230 
231             numSendersLeft = fNumActiveSenders;
232 
233             SkASSERT(fRecordings.size() >= fCurHandled);
234             if (fRecordings.size() > fCurHandled) {
235                 recording = fRecordings[fCurHandled++].get();
236             }
237         }
238 
239         if (recording) {
240             context->insertRecording({recording});
241             return true;  // continue looping
242         }
243 
244         return SkToBool(numSendersLeft); // continue looping if there are still active senders
245     }
246 
247     SkMutex fLock;
248     SkSemaphore fWorkAvailable;
249 
250     skia_private::TArray<std::unique_ptr<Recording>> fRecordings SK_GUARDED_BY(fLock);
251     int fCurHandled SK_GUARDED_BY(fLock) = 0;
252     int fNumActiveSenders SK_GUARDED_BY(fLock);
253 };
254 
compile_gradients(std::unique_ptr<Recorder> recorder,sk_sp<Listener> listener,bool permute,skiatest::Reporter *,int)255 void compile_gradients(std::unique_ptr<Recorder> recorder,
256                        sk_sp<Listener> listener,
257                        bool permute,
258                        skiatest::Reporter* /* reporter */,
259                        int /* threadID */) {
260     std::array<Combo, kNumDiffPipelines> combos;
261 
262     int i = 0;
263     for (auto createOptionsMtd : { linear, radial, sweep, conical }) {
264         for (int numStops: { 2, 7, kMaxNumStops }) {
265             combos[i++] = { createOptionsMtd, numStops };
266         }
267     }
268 
269     if (permute) {
270         std::random_device rd;
271         std::mt19937 g(rd());
272 
273         std::shuffle(combos.begin(), combos.end(), g);
274     }
275 
276     SkFont font(ToolUtils::DefaultPortableTypeface(), /* size= */ 16);
277 
278     const char text[] = "hambur1";
279     sk_sp<SkTextBlob> blob = SkTextBlob::MakeFromText(text, strlen(text), font);
280 
281     SkImageInfo ii = SkImageInfo::Make(16, 16,
282                                        kBGRA_8888_SkColorType,
283                                        kPremul_SkAlphaType);
284 
285     sk_sp<SkSurface> surf = SkSurfaces::RenderTarget(recorder.get(), ii,
286                                                      skgpu::Mipmapped::kNo,
287                                                      /* surfaceProps= */ nullptr);
288     SkCanvas* canvas = surf->getCanvas();
289 
290     for (auto c : combos) {
291         auto [paint, _] = c.fCreateOptionsMtd(c.fNumStops);
292 
293         canvas->drawTextBlob(blob, 0, 16, paint);
294 
295         // This will trigger pipeline creation via TaskList::prepareResources
296         std::unique_ptr<skgpu::graphite::Recording> recording = recorder->snap();
297 
298         listener->addRecording(std::move(recording));
299     }
300 
301     listener->deregister();
302 }
303 
run_test(Context * context,skiatest::Reporter * reporter,int numPurgingThreads,int numRecordingThreads,int numPrecompileThreads,bool permute)304 void run_test(Context* context,
305               skiatest::Reporter* reporter,
306               int numPurgingThreads,
307               int numRecordingThreads,
308               int numPrecompileThreads,
309               bool permute) {
310     const int totNumThreads = numPurgingThreads + numRecordingThreads + numPrecompileThreads;
311 
312     sk_sp<Listener> listener;
313     if (numRecordingThreads) {
314         listener = sk_make_sp<Listener>(numRecordingThreads);
315     }
316 
317     std::atomic_bool keepPurging = true; // controls the looping in the purging thread(s)
318 
319     std::vector<std::thread> threads;
320     threads.reserve(totNumThreads);
321 
322     int threadID = 0;
323     for (int i = 0; i < numPurgingThreads; ++i, ++threadID) {
324         std::unique_ptr<PrecompileContext> precompileContext = context->makePrecompileContext();
325 
326         threads.push_back(std::thread(purge_on_thread,
327                                       std::move(precompileContext),
328                                       &keepPurging,
329                                       reporter,
330                                       threadID));
331     }
332     for (int i = 0; i < numRecordingThreads; ++i, ++threadID) {
333         std::unique_ptr<Recorder> recorder = context->makeRecorder();
334 
335         threads.push_back(std::thread(compile_gradients,
336                                       std::move(recorder),
337                                       listener,
338                                       permute,
339                                       reporter,
340                                       threadID));
341     }
342     for (int i = 0; i < numPrecompileThreads; ++i, ++threadID) {
343         std::unique_ptr<PrecompileContext> precompileContext = context->makePrecompileContext();
344 
345         threads.push_back(std::thread(precompile_gradients,
346                                       std::move(precompileContext),
347                                       permute,
348                                       reporter,
349                                       threadID));
350     }
351 
352     // Process the work generated by the recording threads
353     if (listener) {
354         listener->insertRecordings(context);
355     }
356 
357     keepPurging = false; // stop the loops in the purging thread(s)
358 
359     for (auto& thread : threads) {
360         if (thread.joinable()) {
361             thread.join();
362         }
363     }
364 
365     context->submit(SyncToCpu::kYes);
366 }
367 
dump_stats(skgpu::BackendApi api,const GlobalCache::PipelineStats & stats)368 [[maybe_unused]] void dump_stats(skgpu::BackendApi api, const GlobalCache::PipelineStats& stats) {
369     SkDebugf("%s ------------------------------------------------------------------------------\n"
370              "CacheHits: %d\n"
371              "CacheMisses: %d\n"
372              "CacheAdditions: %d\n"
373              "Races: %d\n"
374              "Purges: %d\n",
375              BackendApiToStr(api),
376              stats.fGraphicsCacheHits,
377              stats.fGraphicsCacheMisses,
378              stats.fGraphicsCacheAdditions,
379              stats.fGraphicsRaces,
380              stats.fGraphicsPurges);
381 }
382 
383 } // anonymous namespace
384 
385 // This test precompiles all four flavors of gradient sequentially but on multiple
386 // threads with the goal of creating cache races.
DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(ThreadedPipelinePrecompileTest,reporter,context,CtsEnforcement::kNever)387 DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(ThreadedPipelinePrecompileTest,
388                                    reporter,
389                                    context,
390                                    CtsEnforcement::kNever) {
391     constexpr int kNumPurgingThreads = 0;
392     constexpr int kNumRecordingThreads = 0;
393     constexpr int kNumPrecompileThreads = 4;
394     constexpr bool kDontPermute = false;
395 
396     run_test(context, reporter, kNumPurgingThreads, kNumRecordingThreads, kNumPrecompileThreads,
397              kDontPermute);
398 
399     const GlobalCache::PipelineStats stats = context->priv().globalCache()->getStats();
400 
401     // The 48 comes from:
402     //     4 gradient flavors (linear, radial, ...) *
403     //     3 types of each flavor (4, 8, N) *
404     //     4 precompile threads
405     REPORTER_ASSERT(reporter, stats.fGraphicsCacheHits + stats.fGraphicsCacheMisses == 48);
406     REPORTER_ASSERT(reporter, stats.fGraphicsCacheAdditions == kNumDiffPipelines);
407     REPORTER_ASSERT(reporter, stats.fGraphicsRaces > 0);
408     REPORTER_ASSERT(reporter, stats.fGraphicsPurges == 0);
409 
410     REPORTER_ASSERT(reporter, stats.fGraphicsCacheMisses == stats.fGraphicsCacheAdditions +
411                                                             stats.fGraphicsRaces);
412 }
413 
414 // This test runs two threads compiling the gradient flavours and two threads
415 // pre-compiling the gradient flavors. This is to exercise the tracking of the
416 // various race combinations (i.e., Normal vs Precompile, Normal vs. Normal, etc.).
DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(ThreadedPipelinePrecompileCompileTest,reporter,context,CtsEnforcement::kNever)417 DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(ThreadedPipelinePrecompileCompileTest,
418                                    reporter,
419                                    context,
420                                    CtsEnforcement::kNever) {
421     constexpr int kNumPurgingThreads = 0;
422     constexpr int kNumRecordingThreads = 2;
423     constexpr int kNumPrecompileThreads = 2;
424     constexpr bool kDontPermute = false;
425 
426     run_test(context, reporter, kNumPurgingThreads, kNumRecordingThreads, kNumPrecompileThreads,
427              kDontPermute);
428 
429     const GlobalCache::PipelineStats stats = context->priv().globalCache()->getStats();
430 
431     // The 48 comes from:
432     //     4 gradient flavors (linear, radial, ...) *
433     //     3 types of each flavor (4, 8, N) *
434     //     (2 normal-compile threads + 2 pre-compile threads)
435     REPORTER_ASSERT(reporter, stats.fGraphicsCacheHits + stats.fGraphicsCacheMisses == 48);
436     REPORTER_ASSERT(reporter, stats.fGraphicsCacheAdditions == kNumDiffPipelines);
437     REPORTER_ASSERT(reporter, stats.fGraphicsRaces > 0);
438     REPORTER_ASSERT(reporter, stats.fGraphicsPurges == 0);
439 
440     REPORTER_ASSERT(reporter, stats.fGraphicsCacheMisses == stats.fGraphicsCacheAdditions +
441                                                             stats.fGraphicsRaces);
442 }
443 
444 // This test compiles the gradient flavors on a thread and then tests out the time-based
445 // purging.
DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(ThreadedPipelineCompilePurgingTest,reporter,context,CtsEnforcement::kNever)446 DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(ThreadedPipelineCompilePurgingTest,
447                                    reporter,
448                                    context,
449                                    CtsEnforcement::kNever) {
450     constexpr int kNumPurgingThreads = 0;
451     constexpr int kNumRecordingThreads = 1;
452     constexpr int kNumPrecompileThreads = 0;
453     constexpr bool kDontPermute = false;
454 
455     std::unique_ptr<PrecompileContext> precompileContext = context->makePrecompileContext();
456 
457     auto begin = std::chrono::steady_clock::now();
458 
459     run_test(context, reporter, kNumPurgingThreads, kNumRecordingThreads, kNumPrecompileThreads,
460              kDontPermute);
461 
462     auto end = std::chrono::steady_clock::now();
463 
464     auto deltaMS = std::chrono::duration_cast<std::chrono::milliseconds>(end - begin);
465 
466     precompileContext->purgePipelinesNotUsedInMs(2*deltaMS);
467 
468     GlobalCache::PipelineStats stats = context->priv().globalCache()->getStats();
469 
470     REPORTER_ASSERT(reporter, stats.fGraphicsCacheHits == 0);
471     REPORTER_ASSERT(reporter, stats.fGraphicsCacheMisses == kNumDiffPipelines);
472     REPORTER_ASSERT(reporter, stats.fGraphicsCacheAdditions == kNumDiffPipelines);
473     REPORTER_ASSERT(reporter, stats.fGraphicsRaces == 0);
474     // Every created Pipeline should've been used since the start of this test
475     REPORTER_ASSERT(reporter, stats.fGraphicsPurges == 0, "num purges: %d", stats.fGraphicsPurges);
476 
477     //--------------------------------------------------------------------------------------------
478     const auto kSleepDuration = std::chrono::milliseconds(1);
479 
480     std::this_thread::sleep_for(kSleepDuration);
481 
482     precompileContext->purgePipelinesNotUsedInMs(kSleepDuration);
483 
484     stats = context->priv().globalCache()->getStats();
485 
486     REPORTER_ASSERT(reporter, stats.fGraphicsCacheHits == 0);
487     REPORTER_ASSERT(reporter, stats.fGraphicsCacheMisses == kNumDiffPipelines);
488     REPORTER_ASSERT(reporter, stats.fGraphicsCacheAdditions == kNumDiffPipelines);
489     REPORTER_ASSERT(reporter, stats.fGraphicsRaces == 0);
490     // None of the created Pipelines should've been used since we started to sleep - so they
491     // all get purged.
492     REPORTER_ASSERT(reporter, stats.fGraphicsPurges == kNumDiffPipelines);
493 }
494 
495 // This test *precompiles* the gradient flavors on a thread and then tests out the time-based
496 // purging.
DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(ThreadedPipelinePrecompilePurgingTest,reporter,context,CtsEnforcement::kNever)497 DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(ThreadedPipelinePrecompilePurgingTest,
498                                    reporter,
499                                    context,
500                                    CtsEnforcement::kNever) {
501     constexpr int kNumPurgingThreads = 0;
502     constexpr int kNumRecordingThreads = 0;
503     constexpr int kNumPrecompileThreads = 1;
504     constexpr bool kDontPermute = false;
505 
506     std::unique_ptr<PrecompileContext> precompileContext = context->makePrecompileContext();
507 
508     auto begin = std::chrono::steady_clock::now();
509 
510     run_test(context, reporter, kNumPurgingThreads, kNumRecordingThreads, kNumPrecompileThreads,
511              kDontPermute);
512 
513     auto end = std::chrono::steady_clock::now();
514 
515     auto deltaMS = std::chrono::duration_cast<std::chrono::milliseconds>(end - begin);
516 
517     precompileContext->purgePipelinesNotUsedInMs(deltaMS);
518 
519     GlobalCache::PipelineStats stats = context->priv().globalCache()->getStats();
520 
521     REPORTER_ASSERT(reporter, stats.fGraphicsCacheHits == 0);
522     REPORTER_ASSERT(reporter, stats.fGraphicsCacheMisses == kNumDiffPipelines);
523     REPORTER_ASSERT(reporter, stats.fGraphicsCacheAdditions == kNumDiffPipelines);
524     REPORTER_ASSERT(reporter, stats.fGraphicsRaces == 0);
525     // Precompilation doesn't count as a use so all the Pipelines will be purged even though
526     // they were created w/in 'deltaMS'
527     REPORTER_ASSERT(reporter, stats.fGraphicsPurges == kNumDiffPipelines);
528 }
529 
530 // This test fires off two compilation threads, two precompilation threads and one
531 // purging thread. This is intended to stress test the Pipeline cache's thread safety and
532 // the purging behavior.
DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(ThreadedPipelinePrecompileCompilePurgingTest,reporter,context,CtsEnforcement::kNever)533 DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(ThreadedPipelinePrecompileCompilePurgingTest,
534                                    reporter,
535                                    context,
536                                    CtsEnforcement::kNever) {
537     constexpr int kNumPurgingThreads = 1;
538     constexpr int kNumRecordingThreads = 2;
539     constexpr int kNumPrecompileThreads = 2;
540     constexpr bool kPermute = true;
541 
542     run_test(context, reporter, kNumPurgingThreads, kNumRecordingThreads, kNumPrecompileThreads,
543              kPermute);
544 
545     GlobalCache::PipelineStats stats = context->priv().globalCache()->getStats();
546 
547     // The 48 comes from:
548     //     4 gradient flavors (linear, radial, ...) *
549     //     3 types of each flavor (4, 8, N) *
550     //     (2 normal-compile threads + 2 pre-compile threads)
551     REPORTER_ASSERT(reporter, stats.fGraphicsCacheHits + stats.fGraphicsCacheMisses == 48);
552     REPORTER_ASSERT(reporter, stats.fGraphicsCacheMisses == stats.fGraphicsCacheAdditions +
553                                                             stats.fGraphicsRaces);
554     // Purges can force recreation of a Pipeline
555     REPORTER_ASSERT(reporter, stats.fGraphicsCacheAdditions >= kNumDiffPipelines);
556     REPORTER_ASSERT(reporter, stats.fGraphicsRaces > 0);
557 }
558 
559 #endif // SK_GRAPHITE
560