go.starlark.net@v0.0.0-20231101134539-556fd59b42f6/starlark/profile.go (about) 1 // Copyright 2019 The Bazel Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package starlark 6 7 // This file defines a simple execution-time profiler for Starlark. 8 // It measures the wall time spent executing Starlark code, and emits a 9 // gzipped protocol message in pprof format (github.com/google/pprof). 10 // 11 // When profiling is enabled, the interpreter calls the profiler to 12 // indicate the start and end of each "span" or time interval. A leaf 13 // function (whether Go or Starlark) has a single span. A function that 14 // calls another function has spans for each interval in which it is the 15 // top of the stack. (A LOAD instruction also ends a span.) 16 // 17 // At the start of a span, the interpreter records the current time in 18 // the thread's topmost frame. At the end of the span, it obtains the 19 // time again and subtracts the span start time. The difference is added 20 // to an accumulator variable in the thread. If the accumulator exceeds 21 // some fixed quantum (10ms, say), the profiler records the current call 22 // stack and sends it to the profiler goroutine, along with the number 23 // of quanta, which are subtracted. For example, if the accumulator 24 // holds 3ms and then a completed span adds 25ms to it, its value is 28ms, 25 // which exceeeds 10ms. The profiler records a stack with the value 20ms 26 // (2 quanta), and the accumulator is left with 8ms. 27 // 28 // The profiler goroutine converts the stacks into the pprof format and 29 // emits a gzip-compressed protocol message to the designated output 30 // file. We use a hand-written streaming proto encoder to avoid 31 // dependencies on pprof and proto, and to avoid the need to 32 // materialize the profile data structure in memory. 33 // 34 // A limitation of this profiler is that it measures wall time, which 35 // does not necessarily correspond to CPU time. A CPU profiler requires 36 // that only running (not runnable) threads are sampled; this is 37 // commonly achieved by having the kernel deliver a (PROF) signal to an 38 // arbitrary running thread, through setitimer(2). The CPU profiler in the 39 // Go runtime uses this mechanism, but it is not possible for a Go 40 // application to register a SIGPROF handler, nor is it possible for a 41 // Go handler for some other signal to read the stack pointer of 42 // the interrupted thread. 43 // 44 // Two caveats: 45 // (1) it is tempting to send the leaf Frame directly to the profiler 46 // goroutine instead of making a copy of the stack, since a Frame is a 47 // spaghetti stack--a linked list. However, as soon as execution 48 // resumes, the stack's Frame.pc values may be mutated, so Frames are 49 // not safe to share with the asynchronous profiler goroutine. 50 // (2) it is tempting to use Callables as keys in a map when tabulating 51 // the pprof protocols's Function entities. However, we cannot assume 52 // that Callables are valid map keys, and furthermore we must not 53 // pin function values in memory indefinitely as this may cause lambda 54 // values to keep their free variables live much longer than necessary. 55 56 // TODO(adonovan): 57 // - make Start/Stop fully thread-safe. 58 // - fix the pc hack. 59 // - experiment with other values of quantum. 60 61 import ( 62 "bufio" 63 "bytes" 64 "compress/gzip" 65 "encoding/binary" 66 "fmt" 67 "io" 68 "log" 69 "reflect" 70 "sync/atomic" 71 "time" 72 "unsafe" 73 74 "go.starlark.net/syntax" 75 ) 76 77 // StartProfile enables time profiling of all Starlark threads, 78 // and writes a profile in pprof format to w. 79 // It must be followed by a call to StopProfiler to stop 80 // the profiler and finalize the profile. 81 // 82 // StartProfile returns an error if profiling was already enabled. 83 // 84 // StartProfile must not be called concurrently with Starlark execution. 85 func StartProfile(w io.Writer) error { 86 if !atomic.CompareAndSwapUint32(&profiler.on, 0, 1) { 87 return fmt.Errorf("profiler already running") 88 } 89 90 // TODO(adonovan): make the API fully concurrency-safe. 91 // The main challenge is racy reads/writes of profiler.events, 92 // and of send/close races on the channel it refers to. 93 // It's easy to solve them with a mutex but harder to do 94 // it efficiently. 95 96 profiler.events = make(chan *profEvent, 1) 97 profiler.done = make(chan error) 98 99 go profile(w) 100 101 return nil 102 } 103 104 // StopProfile stops the profiler started by a prior call to 105 // StartProfile and finalizes the profile. It returns an error if the 106 // profile could not be completed. 107 // 108 // StopProfile must not be called concurrently with Starlark execution. 109 func StopProfile() error { 110 // Terminate the profiler goroutine and get its result. 111 close(profiler.events) 112 err := <-profiler.done 113 114 profiler.done = nil 115 profiler.events = nil 116 atomic.StoreUint32(&profiler.on, 0) 117 118 return err 119 } 120 121 // globals 122 var profiler struct { 123 on uint32 // nonzero => profiler running 124 events chan *profEvent // profile events from interpreter threads 125 done chan error // indicates profiler goroutine is ready 126 } 127 128 func (thread *Thread) beginProfSpan() { 129 if profiler.events == nil { 130 return // profiling not enabled 131 } 132 133 thread.frameAt(0).spanStart = nanotime() 134 } 135 136 // TODO(adonovan): experiment with smaller values, 137 // which trade space and time for greater precision. 138 const quantum = 10 * time.Millisecond 139 140 func (thread *Thread) endProfSpan() { 141 if profiler.events == nil { 142 return // profiling not enabled 143 } 144 145 // Add the span to the thread's accumulator. 146 thread.proftime += time.Duration(nanotime() - thread.frameAt(0).spanStart) 147 if thread.proftime < quantum { 148 return 149 } 150 151 // Only record complete quanta. 152 n := thread.proftime / quantum 153 thread.proftime -= n * quantum 154 155 // Copy the stack. 156 // (We can't save thread.frame because its pc will change.) 157 ev := &profEvent{ 158 thread: thread, 159 time: n * quantum, 160 } 161 ev.stack = ev.stackSpace[:0] 162 for i := range thread.stack { 163 fr := thread.frameAt(i) 164 ev.stack = append(ev.stack, profFrame{ 165 pos: fr.Position(), 166 fn: fr.Callable(), 167 pc: fr.pc, 168 }) 169 } 170 171 profiler.events <- ev 172 } 173 174 type profEvent struct { 175 thread *Thread // currently unused 176 time time.Duration 177 stack []profFrame 178 stackSpace [8]profFrame // initial space for stack 179 } 180 181 type profFrame struct { 182 fn Callable // don't hold this live for too long (prevents GC of lambdas) 183 pc uint32 // program counter (Starlark frames only) 184 pos syntax.Position // position of pc within this frame 185 } 186 187 // profile is the profiler goroutine. 188 // It runs until StopProfiler is called. 189 func profile(w io.Writer) { 190 // Field numbers from pprof protocol. 191 // See https://github.com/google/pprof/blob/master/proto/profile.proto 192 const ( 193 Profile_sample_type = 1 // repeated ValueType 194 Profile_sample = 2 // repeated Sample 195 Profile_mapping = 3 // repeated Mapping 196 Profile_location = 4 // repeated Location 197 Profile_function = 5 // repeated Function 198 Profile_string_table = 6 // repeated string 199 Profile_time_nanos = 9 // int64 200 Profile_duration_nanos = 10 // int64 201 Profile_period_type = 11 // ValueType 202 Profile_period = 12 // int64 203 204 ValueType_type = 1 // int64 205 ValueType_unit = 2 // int64 206 207 Sample_location_id = 1 // repeated uint64 208 Sample_value = 2 // repeated int64 209 Sample_label = 3 // repeated Label 210 211 Label_key = 1 // int64 212 Label_str = 2 // int64 213 Label_num = 3 // int64 214 Label_num_unit = 4 // int64 215 216 Location_id = 1 // uint64 217 Location_mapping_id = 2 // uint64 218 Location_address = 3 // uint64 219 Location_line = 4 // repeated Line 220 221 Line_function_id = 1 // uint64 222 Line_line = 2 // int64 223 224 Function_id = 1 // uint64 225 Function_name = 2 // int64 226 Function_system_name = 3 // int64 227 Function_filename = 4 // int64 228 Function_start_line = 5 // int64 229 ) 230 231 bufw := bufio.NewWriter(w) // write file in 4KB (not 240B flate-sized) chunks 232 gz := gzip.NewWriter(bufw) 233 enc := protoEncoder{w: gz} 234 235 // strings 236 stringIndex := make(map[string]int64) 237 str := func(s string) int64 { 238 i, ok := stringIndex[s] 239 if !ok { 240 i = int64(len(stringIndex)) 241 enc.string(Profile_string_table, s) 242 stringIndex[s] = i 243 } 244 return i 245 } 246 str("") // entry 0 247 248 // functions 249 // 250 // function returns the ID of a Callable for use in Line.FunctionId. 251 // The ID is the same as the function's logical address, 252 // which is supplied by the caller to avoid the need to recompute it. 253 functionId := make(map[uintptr]uint64) 254 function := func(fn Callable, addr uintptr) uint64 { 255 id, ok := functionId[addr] 256 if !ok { 257 id = uint64(addr) 258 259 var pos syntax.Position 260 if fn, ok := fn.(callableWithPosition); ok { 261 pos = fn.Position() 262 } 263 264 name := fn.Name() 265 if name == "<toplevel>" { 266 name = pos.Filename() 267 } 268 269 nameIndex := str(name) 270 271 fun := new(bytes.Buffer) 272 funenc := protoEncoder{w: fun} 273 funenc.uint(Function_id, id) 274 funenc.int(Function_name, nameIndex) 275 funenc.int(Function_system_name, nameIndex) 276 funenc.int(Function_filename, str(pos.Filename())) 277 funenc.int(Function_start_line, int64(pos.Line)) 278 enc.bytes(Profile_function, fun.Bytes()) 279 280 functionId[addr] = id 281 } 282 return id 283 } 284 285 // locations 286 // 287 // location returns the ID of the location denoted by fr. 288 // For Starlark frames, this is the Frame pc. 289 locationId := make(map[uintptr]uint64) 290 location := func(fr profFrame) uint64 { 291 fnAddr := profFuncAddr(fr.fn) 292 293 // For Starlark functions, the frame position 294 // represents the current PC value. 295 // Mix it into the low bits of the address. 296 // This is super hacky and may result in collisions 297 // in large functions or if functions are numerous. 298 // TODO(adonovan): fix: try making this cleaner by treating 299 // each bytecode segment as a Profile.Mapping. 300 pcAddr := fnAddr 301 if _, ok := fr.fn.(*Function); ok { 302 pcAddr = (pcAddr << 16) ^ uintptr(fr.pc) 303 } 304 305 id, ok := locationId[pcAddr] 306 if !ok { 307 id = uint64(pcAddr) 308 309 line := new(bytes.Buffer) 310 lineenc := protoEncoder{w: line} 311 lineenc.uint(Line_function_id, function(fr.fn, fnAddr)) 312 lineenc.int(Line_line, int64(fr.pos.Line)) 313 loc := new(bytes.Buffer) 314 locenc := protoEncoder{w: loc} 315 locenc.uint(Location_id, id) 316 locenc.uint(Location_address, uint64(pcAddr)) 317 locenc.bytes(Location_line, line.Bytes()) 318 enc.bytes(Profile_location, loc.Bytes()) 319 320 locationId[pcAddr] = id 321 } 322 return id 323 } 324 325 wallNanos := new(bytes.Buffer) 326 wnenc := protoEncoder{w: wallNanos} 327 wnenc.int(ValueType_type, str("wall")) 328 wnenc.int(ValueType_unit, str("nanoseconds")) 329 330 // informational fields of Profile 331 enc.bytes(Profile_sample_type, wallNanos.Bytes()) 332 enc.int(Profile_period, quantum.Nanoseconds()) // magnitude of sampling period 333 enc.bytes(Profile_period_type, wallNanos.Bytes()) // dimension and unit of period 334 enc.int(Profile_time_nanos, time.Now().UnixNano()) // start (real) time of profile 335 336 startNano := nanotime() 337 338 // Read profile events from the channel 339 // until it is closed by StopProfiler. 340 for e := range profiler.events { 341 sample := new(bytes.Buffer) 342 sampleenc := protoEncoder{w: sample} 343 sampleenc.int(Sample_value, e.time.Nanoseconds()) // wall nanoseconds 344 for _, fr := range e.stack { 345 sampleenc.uint(Sample_location_id, location(fr)) 346 } 347 enc.bytes(Profile_sample, sample.Bytes()) 348 } 349 350 endNano := nanotime() 351 enc.int(Profile_duration_nanos, endNano-startNano) 352 353 err := gz.Close() // Close reports any prior write error 354 if flushErr := bufw.Flush(); err == nil { 355 err = flushErr 356 } 357 profiler.done <- err 358 } 359 360 // nanotime returns the time in nanoseconds since epoch. 361 // It is implemented by runtime.nanotime using the linkname hack; 362 // runtime.nanotime is defined for all OSs/ARCHS and uses the 363 // monotonic system clock, which there is no portable way to access. 364 // Should that function ever go away, these alternatives exist: 365 // 366 // // POSIX only. REALTIME not MONOTONIC. 17ns. 367 // var tv syscall.Timeval 368 // syscall.Gettimeofday(&tv) // can't fail 369 // return tv.Nano() 370 // 371 // // Portable. REALTIME not MONOTONIC. 46ns. 372 // return time.Now().Nanoseconds() 373 // 374 // // POSIX only. Adds a dependency. 375 // import "golang.org/x/sys/unix" 376 // var ts unix.Timespec 377 // unix.ClockGettime(CLOCK_MONOTONIC, &ts) // can't fail 378 // return unix.TimespecToNsec(ts) 379 // 380 //go:linkname nanotime runtime.nanotime 381 func nanotime() int64 382 383 // profFuncAddr returns the canonical "address" 384 // of a Callable for use by the profiler. 385 func profFuncAddr(fn Callable) uintptr { 386 switch fn := fn.(type) { 387 case *Builtin: 388 return reflect.ValueOf(fn.fn).Pointer() 389 case *Function: 390 return uintptr(unsafe.Pointer(fn.funcode)) 391 } 392 393 // User-defined callable types are typically of 394 // of kind pointer-to-struct. Handle them specially. 395 if v := reflect.ValueOf(fn); v.Type().Kind() == reflect.Ptr { 396 return v.Pointer() 397 } 398 399 // Address zero is reserved by the protocol. 400 // Use 1 for callables we don't recognize. 401 log.Printf("Starlark profiler: no address for Callable %T", fn) 402 return 1 403 } 404 405 // We encode the protocol message by hand to avoid making 406 // the interpreter depend on both github.com/google/pprof 407 // and github.com/golang/protobuf. 408 // 409 // This also avoids the need to materialize a protocol message object 410 // tree of unbounded size and serialize it all at the end. 411 // The pprof format appears to have been designed to 412 // permit streaming implementations such as this one. 413 // 414 // See https://developers.google.com/protocol-buffers/docs/encoding. 415 type protoEncoder struct { 416 w io.Writer // *bytes.Buffer or *gzip.Writer 417 tmp [binary.MaxVarintLen64]byte 418 } 419 420 func (e *protoEncoder) uvarint(x uint64) { 421 n := binary.PutUvarint(e.tmp[:], x) 422 e.w.Write(e.tmp[:n]) 423 } 424 425 func (e *protoEncoder) tag(field, wire uint) { 426 e.uvarint(uint64(field<<3 | wire)) 427 } 428 429 func (e *protoEncoder) string(field uint, s string) { 430 e.tag(field, 2) // length-delimited 431 e.uvarint(uint64(len(s))) 432 io.WriteString(e.w, s) 433 } 434 435 func (e *protoEncoder) bytes(field uint, b []byte) { 436 e.tag(field, 2) // length-delimited 437 e.uvarint(uint64(len(b))) 438 e.w.Write(b) 439 } 440 441 func (e *protoEncoder) uint(field uint, x uint64) { 442 e.tag(field, 0) // varint 443 e.uvarint(x) 444 } 445 446 func (e *protoEncoder) int(field uint, x int64) { 447 e.tag(field, 0) // varint 448 e.uvarint(uint64(x)) 449 }