1 /*
2 * Copyright 2016 Google Inc.
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 "GrCaps.h"
9 #include "GrContextFactory.h"
10 #include "Benchmark.h"
11 #include "ResultsWriter.h"
12 #include "SkCommandLineFlags.h"
13 #include "SkOSFile.h"
14 #include "SkStream.h"
15 #include "SkSurface.h"
16 #include "SkTime.h"
17 #include "SkTLList.h"
18 #include "SkThreadUtils.h"
19 #include "Stats.h"
20 #include "Timer.h"
21 #include "VisualSKPBench.h"
22 #include "gl/GrGLDefines.h"
23 #include "../private/SkMutex.h"
24 #include "../private/SkSemaphore.h"
25 #include "../private/SkGpuFenceSync.h"
26
27 // posix only for now
28 #include <unistd.h>
29 #include <sys/types.h>
30 #include <sys/wait.h>
31
32 /*
33 * This is an experimental GPU only benchmarking program. The initial implementation will only
34 * support SKPs.
35 */
36
37 // To get image decoders linked in we have to do the below magic
38 #include "SkForceLinking.h"
39 #include "SkImageDecoder.h"
40 __SK_FORCE_IMAGE_DECODER_LINKING;
41
42 static const int kAutoTuneLoops = 0;
43
44 static const int kDefaultLoops =
45 #ifdef SK_DEBUG
46 1;
47 #else
48 kAutoTuneLoops;
49 #endif
50
loops_help_txt()51 static SkString loops_help_txt() {
52 SkString help;
53 help.printf("Number of times to run each bench. Set this to %d to auto-"
54 "tune for each bench. Timings are only reported when auto-tuning.",
55 kAutoTuneLoops);
56 return help;
57 }
58
59 DEFINE_string(skps, "skps", "Directory to read skps from.");
60 DEFINE_string2(match, m, nullptr,
61 "[~][^]substring[$] [...] of GM name to run.\n"
62 "Multiple matches may be separated by spaces.\n"
63 "~ causes a matching bench to always be skipped\n"
64 "^ requires the start of the bench to match\n"
65 "$ requires the end of the bench to match\n"
66 "^ and $ requires an exact match\n"
67 "If a bench does not match any list entry,\n"
68 "it is skipped unless some list entry starts with ~");
69 DEFINE_int32(gpuFrameLag, 5, "If unknown, estimated maximum number of frames GPU allows to lag.");
70 DEFINE_int32(samples, 10, "Number of samples to measure for each bench.");
71 DEFINE_int32(maxLoops, 1000000, "Never run a bench more times than this.");
72 DEFINE_int32(loops, kDefaultLoops, loops_help_txt().c_str());
73 DEFINE_double(gpuMs, 5, "Target bench time in millseconds for GPU.");
74 DEFINE_string2(writePath, w, "", "If set, write bitmaps here as .pngs.");
75 DEFINE_bool(useBackgroundThread, true, "If false, kilobench will time cpu / gpu work together");
76 DEFINE_bool(useMultiProcess, true, "If false, kilobench will run all tests in one process");
77
humanize(double ms)78 static SkString humanize(double ms) {
79 return HumanizeMs(ms);
80 }
81 #define HUMANIZE(ms) humanize(ms).c_str()
82
83 namespace kilobench {
84 class BenchmarkStream {
85 public:
BenchmarkStream()86 BenchmarkStream() : fCurrentSKP(0) {
87 for (int i = 0; i < FLAGS_skps.count(); i++) {
88 if (SkStrEndsWith(FLAGS_skps[i], ".skp")) {
89 fSKPs.push_back() = FLAGS_skps[i];
90 } else {
91 SkOSFile::Iter it(FLAGS_skps[i], ".skp");
92 SkString path;
93 while (it.next(&path)) {
94 fSKPs.push_back() = SkOSPath::Join(FLAGS_skps[0], path.c_str());
95 }
96 }
97 }
98 }
99
next()100 Benchmark* next() {
101 Benchmark* bench = nullptr;
102 // skips non matching benches
103 while ((bench = this->innerNext()) &&
104 (SkCommandLineFlags::ShouldSkip(FLAGS_match, bench->getUniqueName()) ||
105 !bench->isSuitableFor(Benchmark::kGPU_Backend))) {
106 delete bench;
107 }
108 return bench;
109 }
110
111 private:
ReadPicture(const char * path,SkAutoTUnref<SkPicture> * pic)112 static bool ReadPicture(const char* path, SkAutoTUnref<SkPicture>* pic) {
113 // Not strictly necessary, as it will be checked again later,
114 // but helps to avoid a lot of pointless work if we're going to skip it.
115 if (SkCommandLineFlags::ShouldSkip(FLAGS_match, path)) {
116 return false;
117 }
118
119 SkAutoTDelete<SkStream> stream(SkStream::NewFromFile(path));
120 if (stream.get() == nullptr) {
121 SkDebugf("Could not read %s.\n", path);
122 return false;
123 }
124
125 pic->reset(SkPicture::CreateFromStream(stream.get()));
126 if (pic->get() == nullptr) {
127 SkDebugf("Could not read %s as an SkPicture.\n", path);
128 return false;
129 }
130 return true;
131 }
132
innerNext()133 Benchmark* innerNext() {
134 // Render skps
135 while (fCurrentSKP < fSKPs.count()) {
136 const SkString& path = fSKPs[fCurrentSKP++];
137 SkAutoTUnref<SkPicture> pic;
138 if (!ReadPicture(path.c_str(), &pic)) {
139 continue;
140 }
141
142 SkString name = SkOSPath::Basename(path.c_str());
143 return new VisualSKPBench(name.c_str(), pic.get());
144 }
145
146 return nullptr;
147 }
148
149 SkTArray<SkString> fSKPs;
150 int fCurrentSKP;
151 };
152
153 struct GPUTarget {
setupkilobench::GPUTarget154 void setup() {
155 fGL->makeCurrent();
156 // Make sure we're done with whatever came before.
157 SK_GL(*fGL, Finish());
158 }
159
beginTimingkilobench::GPUTarget160 SkCanvas* beginTiming(SkCanvas* canvas) { return canvas; }
161
endTimingkilobench::GPUTarget162 void endTiming(bool usePlatformSwapBuffers) {
163 if (fGL) {
164 SK_GL(*fGL, Flush());
165 if (usePlatformSwapBuffers) {
166 fGL->swapBuffers();
167 } else {
168 fGL->waitOnSyncOrSwap();
169 }
170 }
171 }
finishkilobench::GPUTarget172 void finish() {
173 SK_GL(*fGL, Finish());
174 }
175
needsFrameTimingkilobench::GPUTarget176 bool needsFrameTiming(int* maxFrameLag) const {
177 if (!fGL->getMaxGpuFrameLag(maxFrameLag)) {
178 // Frame lag is unknown.
179 *maxFrameLag = FLAGS_gpuFrameLag;
180 }
181 return true;
182 }
183
initkilobench::GPUTarget184 bool init(Benchmark* bench, GrContextFactory* factory, bool useDfText,
185 GrContextFactory::GLContextType ctxType,
186 GrContextFactory::GLContextOptions ctxOptions, int numSamples) {
187 GrContext* context = factory->get(ctxType, ctxOptions);
188 int maxRTSize = context->caps()->maxRenderTargetSize();
189 SkImageInfo info = SkImageInfo::Make(SkTMin(bench->getSize().fX, maxRTSize),
190 SkTMin(bench->getSize().fY, maxRTSize),
191 kN32_SkColorType, kPremul_SkAlphaType);
192 uint32_t flags = useDfText ? SkSurfaceProps::kUseDeviceIndependentFonts_Flag :
193 0;
194 SkSurfaceProps props(flags, SkSurfaceProps::kLegacyFontHost_InitType);
195 fSurface.reset(SkSurface::NewRenderTarget(context,
196 SkBudgeted::kNo, info,
197 numSamples, &props));
198 fGL = factory->getContextInfo(ctxType, ctxOptions).fGLContext;
199 if (!fSurface.get()) {
200 return false;
201 }
202
203 // Kilobench should only be used on platforms with fence sync support
204 SkASSERT(fGL->fenceSyncSupport());
205 return true;
206 }
207
getCanvaskilobench::GPUTarget208 SkCanvas* getCanvas() const {
209 if (!fSurface.get()) {
210 return nullptr;
211 }
212 return fSurface->getCanvas();
213 }
214
capturePixelskilobench::GPUTarget215 bool capturePixels(SkBitmap* bmp) {
216 SkCanvas* canvas = this->getCanvas();
217 if (!canvas) {
218 return false;
219 }
220 bmp->setInfo(canvas->imageInfo());
221 if (!canvas->readPixels(bmp, 0, 0)) {
222 SkDebugf("Can't read canvas pixels.\n");
223 return false;
224 }
225 return true;
226 }
227
glkilobench::GPUTarget228 SkGLContext* gl() { return fGL; }
229
230 private:
231 SkGLContext* fGL;
232 SkAutoTDelete<SkSurface> fSurface;
233 };
234
write_canvas_png(GPUTarget * target,const SkString & filename)235 static bool write_canvas_png(GPUTarget* target, const SkString& filename) {
236
237 if (filename.isEmpty()) {
238 return false;
239 }
240 if (target->getCanvas() &&
241 kUnknown_SkColorType == target->getCanvas()->imageInfo().colorType()) {
242 return false;
243 }
244
245 SkBitmap bmp;
246
247 if (!target->capturePixels(&bmp)) {
248 return false;
249 }
250
251 SkString dir = SkOSPath::Dirname(filename.c_str());
252 if (!sk_mkdir(dir.c_str())) {
253 SkDebugf("Can't make dir %s.\n", dir.c_str());
254 return false;
255 }
256 SkFILEWStream stream(filename.c_str());
257 if (!stream.isValid()) {
258 SkDebugf("Can't write %s.\n", filename.c_str());
259 return false;
260 }
261 if (!SkImageEncoder::EncodeStream(&stream, bmp, SkImageEncoder::kPNG_Type, 100)) {
262 SkDebugf("Can't encode a PNG.\n");
263 return false;
264 }
265 return true;
266 }
267
detect_forever_loops(int loops)268 static int detect_forever_loops(int loops) {
269 // look for a magic run-forever value
270 if (loops < 0) {
271 loops = SK_MaxS32;
272 }
273 return loops;
274 }
275
clamp_loops(int loops)276 static int clamp_loops(int loops) {
277 if (loops < 1) {
278 SkDebugf("ERROR: clamping loops from %d to 1. "
279 "There's probably something wrong with the bench.\n", loops);
280 return 1;
281 }
282 if (loops > FLAGS_maxLoops) {
283 SkDebugf("WARNING: clamping loops from %d to FLAGS_maxLoops, %d.\n", loops, FLAGS_maxLoops);
284 return FLAGS_maxLoops;
285 }
286 return loops;
287 }
288
now_ms()289 static double now_ms() { return SkTime::GetNSecs() * 1e-6; }
290
291 struct TimingThread {
TimingThreadkilobench::TimingThread292 TimingThread(SkGLContext* mainContext)
293 : fFenceSync(mainContext->fenceSync())
294 , fMainContext(mainContext)
295 , fDone(false) {}
296
Loopkilobench::TimingThread297 static void Loop(void* data) {
298 TimingThread* timingThread = reinterpret_cast<TimingThread*>(data);
299 timingThread->timingLoop();
300 }
301
302 // To ensure waiting for the sync actually does something, we check to make sure the we exceed
303 // some small value
304 const double kMinElapsed = 1e-6;
sanitykilobench::TimingThread305 bool sanity(double start) const {
306 double elapsed = now_ms() - start;
307 return elapsed > kMinElapsed;
308 }
309
waitFencekilobench::TimingThread310 void waitFence(SkPlatformGpuFence sync) {
311 SkDEBUGCODE(double start = now_ms());
312 fFenceSync->waitFence(sync, false);
313 SkASSERT(sanity(start));
314 }
315
timingLoopkilobench::TimingThread316 void timingLoop() {
317 // Create a context which shares display lists with the main thread
318 SkAutoTDelete<SkGLContext> glContext(SkCreatePlatformGLContext(kNone_GrGLStandard,
319 fMainContext));
320 glContext->makeCurrent();
321
322 // Basic timing methodology is:
323 // 1) Wait on semaphore until main thread indicates its time to start timing the frame
324 // 2) Wait on frame start sync, record time. This is start of the frame.
325 // 3) Wait on semaphore until main thread indicates its time to finish timing the frame
326 // 4) Wait on frame end sync, record time. FrameEndTime - FrameStartTime = frame time
327 // 5) Wait on semaphore until main thread indicates we should time the next frame or quit
328 while (true) {
329 fSemaphore.wait();
330
331 // get start sync
332 SkPlatformGpuFence startSync = this->popStartSync();
333
334 // wait on sync
335 this->waitFence(startSync);
336 double start = kilobench::now_ms();
337
338 // do we want to sleep here?
339 // wait for end sync
340 fSemaphore.wait();
341
342 // get end sync
343 SkPlatformGpuFence endSync = this->popEndSync();
344
345 // wait on sync
346 this->waitFence(endSync);
347 double elapsed = kilobench::now_ms() - start;
348
349 // No mutex needed, client won't touch timings until we're done
350 fTimings.push_back(elapsed);
351
352 // clean up fences
353 fFenceSync->deleteFence(startSync);
354 fFenceSync->deleteFence(endSync);
355
356 fSemaphore.wait();
357 if (this->isDone()) {
358 break;
359 }
360 }
361 }
362
pushStartSynckilobench::TimingThread363 void pushStartSync() { this->pushSync(&fFrameStartSyncs, &fFrameStartSyncsMutex); }
364
popStartSynckilobench::TimingThread365 SkPlatformGpuFence popStartSync() {
366 return this->popSync(&fFrameStartSyncs, &fFrameStartSyncsMutex);
367 }
368
pushEndSynckilobench::TimingThread369 void pushEndSync() { this->pushSync(&fFrameEndSyncs, &fFrameEndSyncsMutex); }
370
popEndSynckilobench::TimingThread371 SkPlatformGpuFence popEndSync() { return this->popSync(&fFrameEndSyncs, &fFrameEndSyncsMutex); }
372
setDonekilobench::TimingThread373 void setDone() {
374 SkAutoMutexAcquire done(fDoneMutex);
375 fDone = true;
376 fSemaphore.signal();
377 }
378
379 typedef SkTLList<SkPlatformGpuFence, 1> SyncQueue;
380
pushSynckilobench::TimingThread381 void pushSync(SyncQueue* queue, SkMutex* mutex) {
382 SkAutoMutexAcquire am(mutex);
383 *queue->addToHead() = fFenceSync->insertFence();
384 fSemaphore.signal();
385 }
386
popSynckilobench::TimingThread387 SkPlatformGpuFence popSync(SyncQueue* queue, SkMutex* mutex) {
388 SkAutoMutexAcquire am(mutex);
389 SkPlatformGpuFence sync = *queue->head();
390 queue->popHead();
391 return sync;
392 }
393
isDonekilobench::TimingThread394 bool isDone() {
395 SkAutoMutexAcquire am1(fFrameStartSyncsMutex);
396 SkAutoMutexAcquire done(fDoneMutex);
397 if (fDone && fFrameStartSyncs.isEmpty()) {
398 return true;
399 } else {
400 return false;
401 }
402 }
403
timingskilobench::TimingThread404 const SkTArray<double>& timings() const { SkASSERT(fDone); return fTimings; }
405
406 private:
407 SkGpuFenceSync* fFenceSync;
408 SkSemaphore fSemaphore;
409 SkMutex fFrameStartSyncsMutex;
410 SyncQueue fFrameStartSyncs;
411 SkMutex fFrameEndSyncsMutex;
412 SyncQueue fFrameEndSyncs;
413 SkTArray<double> fTimings;
414 SkMutex fDoneMutex;
415 SkGLContext* fMainContext;
416 bool fDone;
417 };
418
time(int loops,Benchmark * bench,GPUTarget * target,TimingThread * timingThread)419 static double time(int loops, Benchmark* bench, GPUTarget* target, TimingThread* timingThread) {
420 SkCanvas* canvas = target->getCanvas();
421 canvas->clear(SK_ColorWHITE);
422 bench->preDraw(canvas);
423
424 if (timingThread) {
425 timingThread->pushStartSync();
426 }
427 double start = now_ms();
428 canvas = target->beginTiming(canvas);
429 bench->draw(loops, canvas);
430 canvas->flush();
431 target->endTiming(timingThread ? true : false);
432
433 double elapsed = now_ms() - start;
434 if (timingThread) {
435 timingThread->pushEndSync();
436 timingThread->setDone();
437 }
438 bench->postDraw(canvas);
439 return elapsed;
440 }
441
442 // TODO For now we don't use the background timing thread to tune loops
setup_gpu_bench(GPUTarget * target,Benchmark * bench,int maxGpuFrameLag)443 static int setup_gpu_bench(GPUTarget* target, Benchmark* bench, int maxGpuFrameLag) {
444 // First, figure out how many loops it'll take to get a frame up to FLAGS_gpuMs.
445 int loops = bench->calculateLoops(FLAGS_loops);
446 if (kAutoTuneLoops == loops) {
447 loops = 1;
448 double elapsed = 0;
449 do {
450 if (1<<30 == loops) {
451 // We're about to wrap. Something's wrong with the bench.
452 loops = 0;
453 break;
454 }
455 loops *= 2;
456 // If the GPU lets frames lag at all, we need to make sure we're timing
457 // _this_ round, not still timing last round.
458 for (int i = 0; i < maxGpuFrameLag; i++) {
459 elapsed = time(loops, bench, target, nullptr);
460 }
461 } while (elapsed < FLAGS_gpuMs);
462
463 // We've overshot at least a little. Scale back linearly.
464 loops = (int)ceil(loops * FLAGS_gpuMs / elapsed);
465 loops = clamp_loops(loops);
466
467 // Make sure we're not still timing our calibration.
468 target->finish();
469 } else {
470 loops = detect_forever_loops(loops);
471 }
472
473 // Pretty much the same deal as the calibration: do some warmup to make
474 // sure we're timing steady-state pipelined frames.
475 for (int i = 0; i < maxGpuFrameLag - 1; i++) {
476 time(loops, bench, target, nullptr);
477 }
478
479 return loops;
480 }
481
482 struct AutoSetupContextBenchAndTarget {
AutoSetupContextBenchAndTargetkilobench::AutoSetupContextBenchAndTarget483 AutoSetupContextBenchAndTarget(Benchmark* bench) : fBenchmark(bench) {
484 GrContextOptions grContextOpts;
485 fCtxFactory.reset(new GrContextFactory(grContextOpts));
486
487 SkAssertResult(fTarget.init(bench, fCtxFactory, false,
488 GrContextFactory::kNative_GLContextType,
489 GrContextFactory::kNone_GLContextOptions, 0));
490
491 fCanvas = fTarget.getCanvas();
492 fTarget.setup();
493
494 bench->perCanvasPreDraw(fCanvas);
495 fTarget.needsFrameTiming(&fMaxFrameLag);
496 }
497
getLoopskilobench::AutoSetupContextBenchAndTarget498 int getLoops() { return setup_gpu_bench(&fTarget, fBenchmark, fMaxFrameLag); }
499
timeSamplekilobench::AutoSetupContextBenchAndTarget500 double timeSample(int loops, TimingThread* timingThread) {
501 for (int i = 0; i < fMaxFrameLag; i++) {
502 time(loops, fBenchmark, &fTarget, timingThread);
503 }
504
505 return time(loops, fBenchmark, &fTarget, timingThread) / loops;
506 }
507
teardownBenchkilobench::AutoSetupContextBenchAndTarget508 void teardownBench() { fBenchmark->perCanvasPostDraw(fCanvas); }
509
510 SkAutoTDelete<GrContextFactory> fCtxFactory;
511 GPUTarget fTarget;
512 SkCanvas* fCanvas;
513 Benchmark* fBenchmark;
514 int fMaxFrameLag;
515 };
516
setup_loops(Benchmark * bench)517 int setup_loops(Benchmark* bench) {
518 AutoSetupContextBenchAndTarget ascbt(bench);
519 int loops = ascbt.getLoops();
520 ascbt.teardownBench();
521
522 if (!FLAGS_writePath.isEmpty() && FLAGS_writePath[0]) {
523 SkString pngFilename = SkOSPath::Join(FLAGS_writePath[0], "gpu");
524 pngFilename = SkOSPath::Join(pngFilename.c_str(), bench->getUniqueName());
525 pngFilename.append(".png");
526 write_canvas_png(&ascbt.fTarget, pngFilename);
527 }
528 return loops;
529 }
530
531 struct Sample {
532 double fCpu;
533 double fGpu;
534 };
535
time_sample(Benchmark * bench,int loops)536 Sample time_sample(Benchmark* bench, int loops) {
537 AutoSetupContextBenchAndTarget ascbt(bench);
538
539 Sample sample;
540 if (FLAGS_useBackgroundThread) {
541 TimingThread timingThread(ascbt.fTarget.gl());
542 SkAutoTDelete<SkThread> nativeThread(new SkThread(TimingThread::Loop, &timingThread));
543 nativeThread->start();
544 sample.fCpu = ascbt.timeSample(loops, &timingThread);
545 nativeThread->join();
546
547 // return the min
548 double min = SK_ScalarMax;
549 for (int i = 0; i < timingThread.timings().count(); i++) {
550 min = SkTMin(min, timingThread.timings()[i]);
551 }
552 sample.fGpu = min;
553 } else {
554 sample.fCpu = ascbt.timeSample(loops, nullptr);
555 }
556
557 ascbt.teardownBench();
558
559 return sample;
560 }
561
562 } // namespace kilobench
563
564 static const int kOutResultSize = 1024;
565
printResult(const SkTArray<double> & samples,int loops,const char * name,const char * mod)566 void printResult(const SkTArray<double>& samples, int loops, const char* name, const char* mod) {
567 SkString newName(name);
568 newName.appendf("_%s", mod);
569 Stats stats(samples);
570 const double stddev_percent = 100 * sqrt(stats.var) / stats.mean;
571 SkDebugf("%d\t%s\t%s\t%s\t%s\t%.0f%%\t%s\t%s\t%s\n"
572 , loops
573 , HUMANIZE(stats.min)
574 , HUMANIZE(stats.median)
575 , HUMANIZE(stats.mean)
576 , HUMANIZE(stats.max)
577 , stddev_percent
578 , stats.plot.c_str()
579 , "gpu"
580 , newName.c_str()
581 );
582 }
583
kilobench_main()584 int kilobench_main() {
585 kilobench::BenchmarkStream benchStream;
586
587 SkDebugf("loops\tmin\tmedian\tmean\tmax\tstddev\t%-*s\tconfig\tbench\n",
588 FLAGS_samples, "samples");
589
590 int descriptors[2];
591 if (pipe(descriptors) != 0) {
592 SkFAIL("Failed to open a pipe\n");
593 }
594
595 while (Benchmark* b = benchStream.next()) {
596 SkAutoTDelete<Benchmark> bench(b);
597
598 int loops = 1;
599 SkTArray<double> cpuSamples;
600 SkTArray<double> gpuSamples;
601 for (int i = 0; i < FLAGS_samples + 1; i++) {
602 // We fork off a new process to setup the grcontext and run the test while we wait
603 if (FLAGS_useMultiProcess) {
604 int childPid = fork();
605 if (childPid > 0) {
606 char result[kOutResultSize];
607 if (read(descriptors[0], result, kOutResultSize) < 0) {
608 SkFAIL("Failed to read from pipe\n");
609 }
610
611 // if samples == 0 then parse # of loops
612 // else parse float
613 if (i == 0) {
614 sscanf(result, "%d", &loops);
615 } else {
616 sscanf(result, "%lf %lf", &cpuSamples.push_back(),
617 &gpuSamples.push_back());
618 }
619
620 // wait until exit
621 int status;
622 waitpid(childPid, &status, 0);
623 } else if (0 == childPid) {
624 char result[kOutResultSize];
625 if (i == 0) {
626 sprintf(result, "%d", kilobench::setup_loops(bench));
627 } else {
628 kilobench::Sample sample = kilobench::time_sample(bench, loops);
629 sprintf(result, "%lf %lf", sample.fCpu, sample.fGpu);
630 }
631
632 // Make sure to write the null terminator
633 if (write(descriptors[1], result, strlen(result) + 1) < 0) {
634 SkFAIL("Failed to write to pipe\n");
635 }
636 return 0;
637 } else {
638 SkFAIL("Fork failed\n");
639 }
640 } else {
641 if (i == 0) {
642 loops = kilobench::setup_loops(bench);
643 } else {
644 kilobench::Sample sample = kilobench::time_sample(bench, loops);
645 cpuSamples.push_back(sample.fCpu);
646 gpuSamples.push_back(sample.fGpu);
647 }
648 }
649 }
650
651 printResult(cpuSamples, loops, bench->getUniqueName(), "cpu");
652 if (FLAGS_useBackgroundThread) {
653 printResult(gpuSamples, loops, bench->getUniqueName(), "gpu");
654 }
655 }
656 return 0;
657 }
658
659 #if !defined SK_BUILD_FOR_IOS
main(int argc,char ** argv)660 int main(int argc, char** argv) {
661 SkCommandLineFlags::Parse(argc, argv);
662 return kilobench_main();
663 }
664 #endif
665