1// Copyright 2019 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "components/viz/common/gpu/metal_api_proxy.h" 6 7#include <objc/objc.h> 8 9#include <map> 10#include <string> 11 12#include "base/debug/crash_logging.h" 13#include "base/mac/foundation_util.h" 14#include "base/memory/ref_counted.h" 15#include "base/metrics/histogram_macros.h" 16#include "base/no_destructor.h" 17#include "base/strings/string_number_conversions.h" 18#include "base/strings/stringprintf.h" 19#include "base/strings/sys_string_conversions.h" 20#include "base/synchronization/condition_variable.h" 21#include "base/trace_event/trace_event.h" 22#include "components/crash/core/common/crash_key.h" 23#include "ui/gl/progress_reporter.h" 24 25namespace { 26 27// State shared between the caller of [MTLDevice newLibraryWithSource:] and its 28// MTLNewLibraryCompletionHandler (and similarly for -[MTLDevice 29// newRenderPipelineStateWithDescriptor:]. The completion handler may be called 30// on another thread, so all members are protected by a lock. Accessed via 31// scoped_refptr to ensure that it exists until its last accessor is gone. 32class API_AVAILABLE(macos(10.11)) AsyncMetalState 33 : public base::RefCountedThreadSafe<AsyncMetalState> { 34 public: 35 AsyncMetalState() : condition_variable(&lock) {} 36 37 // All members may only be accessed while |lock| is held. 38 base::Lock lock; 39 base::ConditionVariable condition_variable; 40 41 // Set to true when the completion handler is called. 42 bool has_result = false; 43 44 // The results of the async operation. These are set only by the first 45 // completion handler to run. 46 id<MTLLibrary> library = nil; 47 id<MTLRenderPipelineState> render_pipeline_state = nil; 48 NSError* error = nil; 49 50 private: 51 friend class base::RefCountedThreadSafe<AsyncMetalState>; 52 ~AsyncMetalState() { DCHECK(has_result); } 53}; 54 55id<MTLLibrary> API_AVAILABLE(macos(10.11)) 56 NewLibraryWithRetry(id<MTLDevice> device, 57 NSString* source, 58 MTLCompileOptions* options, 59 __autoreleasing NSError** error, 60 gl::ProgressReporter* progress_reporter) { 61 SCOPED_UMA_HISTOGRAM_TIMER("Gpu.MetalProxy.NewLibraryTime"); 62 const base::TimeTicks start_time = base::TimeTicks::Now(); 63 auto state = base::MakeRefCounted<AsyncMetalState>(); 64 65 // The completion handler will signal the condition variable we will wait 66 // on. Note that completionHandler will hold a reference to |state|. 67 MTLNewLibraryCompletionHandler completionHandler = 68 ^(id<MTLLibrary> library, NSError* error) { 69 base::AutoLock lock(state->lock); 70 state->has_result = true; 71 state->library = [library retain]; 72 state->error = [error retain]; 73 state->condition_variable.Signal(); 74 }; 75 76 // Request asynchronous compilation. Note that |completionHandler| may be 77 // called from within this function call, or it may be called from a 78 // different thread. 79 if (progress_reporter) 80 progress_reporter->ReportProgress(); 81 [device newLibraryWithSource:source 82 options:options 83 completionHandler:completionHandler]; 84 85 // Suppress the watchdog timer for kTimeout by reporting progress every 86 // half-second. After that, allow it to kill the the GPU process. 87 constexpr base::TimeDelta kTimeout = base::TimeDelta::FromSeconds(60); 88 constexpr base::TimeDelta kWaitPeriod = 89 base::TimeDelta::FromMilliseconds(500); 90 while (true) { 91 if (base::TimeTicks::Now() - start_time < kTimeout && progress_reporter) 92 progress_reporter->ReportProgress(); 93 base::AutoLock lock(state->lock); 94 if (state->has_result) { 95 *error = [state->error autorelease]; 96 return state->library; 97 } 98 state->condition_variable.TimedWait(kWaitPeriod); 99 } 100} 101 102id<MTLRenderPipelineState> API_AVAILABLE(macos(10.11)) 103 NewRenderPipelineStateWithRetry(id<MTLDevice> device, 104 MTLRenderPipelineDescriptor* descriptor, 105 __autoreleasing NSError** error, 106 gl::ProgressReporter* progress_reporter) { 107 // This function is almost-identical to the above NewLibraryWithRetry. See 108 // comments in that function. 109 SCOPED_UMA_HISTOGRAM_TIMER("Gpu.MetalProxy.NewRenderPipelineStateTime"); 110 const base::TimeTicks start_time = base::TimeTicks::Now(); 111 auto state = base::MakeRefCounted<AsyncMetalState>(); 112 MTLNewRenderPipelineStateCompletionHandler completionHandler = 113 ^(id<MTLRenderPipelineState> render_pipeline_state, NSError* error) { 114 base::AutoLock lock(state->lock); 115 state->has_result = true; 116 state->render_pipeline_state = [render_pipeline_state retain]; 117 state->error = [error retain]; 118 state->condition_variable.Signal(); 119 }; 120 if (progress_reporter) 121 progress_reporter->ReportProgress(); 122 [device newRenderPipelineStateWithDescriptor:descriptor 123 completionHandler:completionHandler]; 124 constexpr base::TimeDelta kTimeout = base::TimeDelta::FromSeconds(60); 125 constexpr base::TimeDelta kWaitPeriod = 126 base::TimeDelta::FromMilliseconds(500); 127 while (true) { 128 if (base::TimeTicks::Now() - start_time < kTimeout && progress_reporter) 129 progress_reporter->ReportProgress(); 130 base::AutoLock lock(state->lock); 131 if (state->has_result) { 132 *error = [state->error autorelease]; 133 return state->render_pipeline_state; 134 } 135 state->condition_variable.TimedWait(kWaitPeriod); 136 } 137} 138 139// Maximum length of a shader to be uploaded with a crash report. 140constexpr uint32_t kShaderCrashDumpLength = 8128; 141 142} // namespace 143 144// A cache of the result of calls to NewLibraryWithRetry. This will store all 145// resulting MTLLibraries indefinitely, and will grow without bound. This is to 146// minimize the number of calls hitting the MTLCompilerService, which is prone 147// to hangs. Should this significantly help the situation, a more robust (and 148// not indefinitely-growing) cache will be added either here or in Skia. 149// https://crbug.com/974219 150class API_AVAILABLE(macos(10.11)) MTLLibraryCache { 151 public: 152 MTLLibraryCache() = default; 153 ~MTLLibraryCache() = default; 154 155 id<MTLLibrary> NewLibraryWithSource(id<MTLDevice> device, 156 NSString* source, 157 MTLCompileOptions* options, 158 __autoreleasing NSError** error, 159 gl::ProgressReporter* progress_reporter) { 160 LibraryKey key(source, options); 161 auto found = libraries_.find(key); 162 if (found != libraries_.end()) { 163 const LibraryData& data = found->second; 164 *error = [[data.error retain] autorelease]; 165 return [data.library retain]; 166 } 167 SCOPED_UMA_HISTOGRAM_TIMER("Gpu.MetalProxy.NewLibraryTime"); 168 id<MTLLibrary> library = 169 NewLibraryWithRetry(device, source, options, error, progress_reporter); 170 LibraryData data(library, *error); 171 libraries_.insert(std::make_pair(key, std::move(data))); 172 return library; 173 } 174 // The number of cache misses is the number of times that we have had to call 175 // the true newLibraryWithSource function. 176 uint64_t CacheMissCount() const { return libraries_.size(); } 177 178 private: 179 struct LibraryKey { 180 LibraryKey(NSString* source, MTLCompileOptions* options) 181 : source_(source, base::scoped_policy::RETAIN), 182 options_(options, base::scoped_policy::RETAIN) {} 183 LibraryKey(const LibraryKey& other) = default; 184 LibraryKey& operator=(const LibraryKey& other) = default; 185 ~LibraryKey() = default; 186 187 bool operator<(const LibraryKey& other) const { 188 switch ([source_ compare:other.source_]) { 189 case NSOrderedAscending: 190 return true; 191 case NSOrderedDescending: 192 return false; 193 case NSOrderedSame: 194 break; 195 } 196#define COMPARE(x) \ 197 if ([options_ x] < [other.options_ x]) \ 198 return true; \ 199 if ([options_ x] > [other.options_ x]) \ 200 return false; 201 COMPARE(fastMathEnabled); 202 COMPARE(languageVersion); 203#undef COMPARE 204 // Skia doesn't set any preprocessor macros, and defining an order on two 205 // NSDictionaries is a lot of code, so just assert that there are no 206 // macros. Should this alleviate https://crbug.com/974219, then a more 207 // robust cache should be implemented. 208 DCHECK_EQ([[options_ preprocessorMacros] count], 0u); 209 return false; 210 } 211 212 private: 213 base::scoped_nsobject<NSString> source_; 214 base::scoped_nsobject<MTLCompileOptions> options_; 215 }; 216 struct LibraryData { 217 LibraryData(id<MTLLibrary> library_, NSError* error_) 218 : library(library_, base::scoped_policy::RETAIN), 219 error(error_, base::scoped_policy::RETAIN) {} 220 LibraryData(const LibraryData& other) = default; 221 LibraryData& operator=(const LibraryData& other) = default; 222 ~LibraryData() = default; 223 224 base::scoped_nsprotocol<id<MTLLibrary>> library; 225 base::scoped_nsobject<NSError> error; 226 }; 227 228 std::map<LibraryKey, LibraryData> libraries_; 229 DISALLOW_COPY_AND_ASSIGN(MTLLibraryCache); 230}; 231 232// Disable protocol warnings and property synthesis warnings. Any unimplemented 233// methods/properties in the MTLDevice protocol will be handled by the 234// -forwardInvocation: method. 235#pragma clang diagnostic push 236#pragma clang diagnostic ignored "-Wprotocol" 237#pragma clang diagnostic ignored "-Wobjc-protocol-property-synthesis" 238 239@implementation MTLDeviceProxy 240- (id)initWithDevice:(id<MTLDevice>)device { 241 if (self = [super init]) { 242 _device.reset(device, base::scoped_policy::RETAIN); 243 _libraryCache = std::make_unique<MTLLibraryCache>(); 244 } 245 return self; 246} 247 248- (void)setProgressReporter:(gl::ProgressReporter*)progressReporter { 249 _progressReporter = progressReporter; 250} 251 252- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector { 253 // Technically, _device is of protocol MTLDevice which inherits from protocol 254 // NSObject, and protocol NSObject does not have -methodSignatureForSelector:. 255 // Assume that the implementing class derives from NSObject. 256 return [base::mac::ObjCCastStrict<NSObject>(_device) 257 methodSignatureForSelector:selector]; 258} 259 260- (void)forwardInvocation:(NSInvocation*)invocation { 261 // The number of methods on MTLDevice is finite and small, so this unbounded 262 // cache is fine. std::map does not move elements on additions to the map, so 263 // the requirement that strings passed to TRACE_EVENT0 don't move is 264 // fulfilled. 265 static base::NoDestructor<std::map<SEL, std::string>> invocationNames; 266 auto& invocationName = (*invocationNames)[invocation.selector]; 267 if (invocationName.empty()) { 268 invocationName = 269 base::StringPrintf("-[MTLDevice %s]", sel_getName(invocation.selector)); 270 } 271 272 TRACE_EVENT0("gpu", invocationName.c_str()); 273 gl::ScopedProgressReporter scoped_reporter(_progressReporter); 274 [invocation invokeWithTarget:_device.get()]; 275} 276 277- (nullable id<MTLLibrary>) 278 newLibraryWithSource:(NSString*)source 279 options:(nullable MTLCompileOptions*)options 280 error:(__autoreleasing NSError**)error { 281 TRACE_EVENT0("gpu", "-[MTLDevice newLibraryWithSource:options:error:]"); 282 283 // Capture the shader's source in a crash key in case newLibraryWithSource 284 // hangs. 285 // https://crbug.com/974219 286 static crash_reporter::CrashKeyString<kShaderCrashDumpLength> shaderKey( 287 "MTLShaderSource"); 288 std::string sourceAsSysString = base::SysNSStringToUTF8(source); 289 if (sourceAsSysString.size() > kShaderCrashDumpLength) 290 DLOG(WARNING) << "Truncating shader in crash log."; 291 292 shaderKey.Set(sourceAsSysString); 293 static crash_reporter::CrashKeyString<16> newLibraryCountKey( 294 "MTLNewLibraryCount"); 295 newLibraryCountKey.Set(base::NumberToString(_libraryCache->CacheMissCount())); 296 297 id<MTLLibrary> library = _libraryCache->NewLibraryWithSource( 298 _device, source, options, error, _progressReporter); 299 shaderKey.Clear(); 300 newLibraryCountKey.Clear(); 301 302 // Shaders from Skia will have either a vertexMain or fragmentMain function. 303 // Save the source and a weak pointer to the function, so we can capture 304 // the shader source in -newRenderPipelineStateWithDescriptor (see further 305 // remarks in that function). 306 base::scoped_nsprotocol<id<MTLFunction>> vertexFunction( 307 [library newFunctionWithName:@"vertexMain"]); 308 if (vertexFunction) { 309 _vertexSourceFunction = vertexFunction; 310 _vertexSource = sourceAsSysString; 311 } 312 base::scoped_nsprotocol<id<MTLFunction>> fragmentFunction( 313 [library newFunctionWithName:@"fragmentMain"]); 314 if (fragmentFunction) { 315 _fragmentSourceFunction = fragmentFunction; 316 _fragmentSource = sourceAsSysString; 317 } 318 319 return library; 320} 321 322- (nullable id<MTLRenderPipelineState>) 323 newRenderPipelineStateWithDescriptor: 324 (MTLRenderPipelineDescriptor*)descriptor 325 error:(__autoreleasing NSError**)error { 326 TRACE_EVENT0("gpu", 327 "-[MTLDevice newRenderPipelineStateWithDescriptor:error:]"); 328 // Capture the vertex and shader source being used. Skia's use pattern is to 329 // compile two MTLLibraries before creating a MTLRenderPipelineState -- one 330 // with vertexMain and the other with fragmentMain. The two immediately 331 // previous -newLibraryWithSource calls should have saved the sources for 332 // these two functions. 333 // https://crbug.com/974219 334 static crash_reporter::CrashKeyString<kShaderCrashDumpLength> vertexShaderKey( 335 "MTLVertexSource"); 336 if (_vertexSourceFunction == [descriptor vertexFunction]) 337 vertexShaderKey.Set(_vertexSource); 338 else 339 DLOG(WARNING) << "Failed to capture vertex shader."; 340 static crash_reporter::CrashKeyString<kShaderCrashDumpLength> 341 fragmentShaderKey("MTLFragmentSource"); 342 if (_fragmentSourceFunction == [descriptor fragmentFunction]) 343 fragmentShaderKey.Set(_fragmentSource); 344 else 345 DLOG(WARNING) << "Failed to capture fragment shader."; 346 static crash_reporter::CrashKeyString<16> newLibraryCountKey( 347 "MTLNewLibraryCount"); 348 newLibraryCountKey.Set(base::NumberToString(_libraryCache->CacheMissCount())); 349 350 SCOPED_UMA_HISTOGRAM_TIMER("Gpu.MetalProxy.NewRenderPipelineStateTime"); 351 id<MTLRenderPipelineState> pipelineState = NewRenderPipelineStateWithRetry( 352 _device, descriptor, error, _progressReporter); 353 354 vertexShaderKey.Clear(); 355 fragmentShaderKey.Clear(); 356 newLibraryCountKey.Clear(); 357 return pipelineState; 358} 359 360@end 361 362#pragma clang diagnostic pop 363