Source File
traceruntime.go
Belonging Package
runtime
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Runtime -> tracer API.
package runtime
import (
_ // for go:linkname
)
// gTraceState is per-G state for the tracer.
type gTraceState struct {
traceSchedResourceState
}
// reset resets the gTraceState for a new goroutine.
func ( *gTraceState) () {
.seq = [2]uint64{}
// N.B. s.statusTraced is managed and cleared separately.
}
// mTraceState is per-M state for the tracer.
type mTraceState struct {
seqlock atomic.Uintptr // seqlock indicating that this M is writing to a trace buffer.
buf [2][traceNumExperiments]*traceBuf // Per-M traceBuf for writing. Indexed by trace.gen%2.
link *m // Snapshot of alllink or freelink.
reentered uint32 // Whether we've reentered tracing from within tracing.
oldthrowsplit bool // gp.throwsplit upon calling traceLocker.writer. For debugging.
}
// pTraceState is per-P state for the tracer.
type pTraceState struct {
traceSchedResourceState
// mSyscallID is the ID of the M this was bound to before entering a syscall.
mSyscallID int64
// maySweep indicates the sweep events should be traced.
// This is used to defer the sweep start event until a span
// has actually been swept.
maySweep bool
// inSweep indicates that at least one sweep event has been traced.
inSweep bool
// swept and reclaimed track the number of bytes swept and reclaimed
// by sweeping in the current sweep loop (while maySweep was true).
swept, reclaimed uintptr
}
// traceLockInit initializes global trace locks.
func traceLockInit() {
// Sharing a lock rank here is fine because they should never be accessed
// together. If they are, we want to find out immediately.
lockInit(&trace.stringTab[0].lock, lockRankTraceStrings)
lockInit(&trace.stringTab[0].tab.mem.lock, lockRankTraceStrings)
lockInit(&trace.stringTab[1].lock, lockRankTraceStrings)
lockInit(&trace.stringTab[1].tab.mem.lock, lockRankTraceStrings)
lockInit(&trace.stackTab[0].tab.mem.lock, lockRankTraceStackTab)
lockInit(&trace.stackTab[1].tab.mem.lock, lockRankTraceStackTab)
lockInit(&trace.typeTab[0].tab.mem.lock, lockRankTraceTypeTab)
lockInit(&trace.typeTab[1].tab.mem.lock, lockRankTraceTypeTab)
lockInit(&trace.lock, lockRankTrace)
}
// lockRankMayTraceFlush records the lock ranking effects of a
// potential call to traceFlush.
//
// nosplit because traceAcquire is nosplit.
//
//go:nosplit
func lockRankMayTraceFlush() {
lockWithRankMayAcquire(&trace.lock, getLockRank(&trace.lock))
}
// traceBlockReason is an enumeration of reasons a goroutine might block.
// This is the interface the rest of the runtime uses to tell the
// tracer why a goroutine blocked. The tracer then propagates this information
// into the trace however it sees fit.
//
// Note that traceBlockReasons should not be compared, since reasons that are
// distinct by name may *not* be distinct by value.
type traceBlockReason uint8
const (
traceBlockGeneric traceBlockReason = iota
traceBlockForever
traceBlockNet
traceBlockSelect
traceBlockCondWait
traceBlockSync
traceBlockChanSend
traceBlockChanRecv
traceBlockGCMarkAssist
traceBlockGCSweep
traceBlockSystemGoroutine
traceBlockPreempted
traceBlockDebugCall
traceBlockUntilGCEnds
traceBlockSleep
traceBlockGCWeakToStrongWait
traceBlockSynctest
)
var traceBlockReasonStrings = [...]string{
traceBlockGeneric: "unspecified",
traceBlockForever: "forever",
traceBlockNet: "network",
traceBlockSelect: "select",
traceBlockCondWait: "sync.(*Cond).Wait",
traceBlockSync: "sync",
traceBlockChanSend: "chan send",
traceBlockChanRecv: "chan receive",
traceBlockGCMarkAssist: "GC mark assist wait for work",
traceBlockGCSweep: "GC background sweeper wait",
traceBlockSystemGoroutine: "system goroutine wait",
traceBlockPreempted: "preempted",
traceBlockDebugCall: "wait for debug call",
traceBlockUntilGCEnds: "wait until GC ends",
traceBlockSleep: "sleep",
traceBlockGCWeakToStrongWait: "GC weak to strong wait",
traceBlockSynctest: "synctest",
}
// traceGoStopReason is an enumeration of reasons a goroutine might yield.
//
// Note that traceGoStopReasons should not be compared, since reasons that are
// distinct by name may *not* be distinct by value.
type traceGoStopReason uint8
const (
traceGoStopGeneric traceGoStopReason = iota
traceGoStopGoSched
traceGoStopPreempted
)
var traceGoStopReasonStrings = [...]string{
traceGoStopGeneric: "unspecified",
traceGoStopGoSched: "runtime.Gosched",
traceGoStopPreempted: "preempted",
}
// traceEnabled returns true if the trace is currently enabled.
//
//go:nosplit
func traceEnabled() bool {
return trace.enabled
}
// traceAllocFreeEnabled returns true if the trace is currently enabled
// and alloc/free events are also enabled.
//
//go:nosplit
func traceAllocFreeEnabled() bool {
return trace.enabledWithAllocFree
}
// traceShuttingDown returns true if the trace is currently shutting down.
func traceShuttingDown() bool {
return trace.shutdown.Load()
}
// traceLocker represents an M writing trace events. While a traceLocker value
// is valid, the tracer observes all operations on the G/M/P or trace events being
// written as happening atomically.
type traceLocker struct {
mp *m
gen uintptr
}
// debugTraceReentrancy checks if the trace is reentrant.
//
// This is optional because throwing in a function makes it instantly
// not inlineable, and we want traceAcquire to be inlineable for
// low overhead when the trace is disabled.
const debugTraceReentrancy = false
// traceAcquire prepares this M for writing one or more trace events.
//
// nosplit because it's called on the syscall path when stack movement is forbidden.
//
//go:nosplit
func traceAcquire() traceLocker {
if !traceEnabled() {
return traceLocker{}
}
return traceAcquireEnabled()
}
// traceAcquireEnabled is the traceEnabled path for traceAcquire. It's explicitly
// broken out to make traceAcquire inlineable to keep the overhead of the tracer
// when it's disabled low.
//
// nosplit because it's called by traceAcquire, which is nosplit.
//
//go:nosplit
func traceAcquireEnabled() traceLocker {
// Any time we acquire a traceLocker, we may flush a trace buffer. But
// buffer flushes are rare. Record the lock edge even if it doesn't happen
// this time.
lockRankMayTraceFlush()
// Prevent preemption.
:= acquirem()
// Check if we're already tracing. It's safe to be reentrant in general,
// because this function (and the invariants of traceLocker.writer) ensure
// that it is.
if .trace.seqlock.Load()%2 == 1 {
.trace.reentered++
return traceLocker{, trace.gen.Load()}
}
// Acquire the trace seqlock. This prevents traceAdvance from moving forward
// until all Ms are observed to be outside of their seqlock critical section.
//
// Note: The seqlock is mutated here and also in traceCPUSample. If you update
// usage of the seqlock here, make sure to also look at what traceCPUSample is
// doing.
:= .trace.seqlock.Add(1)
if debugTraceReentrancy && %2 != 1 {
throw("bad use of trace.seqlock")
}
// N.B. This load of gen appears redundant with the one in traceEnabled.
// However, it's very important that the gen we use for writing to the trace
// is acquired under a traceLocker so traceAdvance can make sure no stale
// gen values are being used.
//
// Because we're doing this load again, it also means that the trace
// might end up being disabled when we load it. In that case we need to undo
// what we did and bail.
:= trace.gen.Load()
if == 0 {
.trace.seqlock.Add(1)
releasem()
return traceLocker{}
}
return traceLocker{, }
}
// ok returns true if the traceLocker is valid (i.e. tracing is enabled).
//
// nosplit because it's called on the syscall path when stack movement is forbidden.
//
//go:nosplit
func ( traceLocker) () bool {
return .gen != 0
}
// traceRelease indicates that this M is done writing trace events.
//
// nosplit because it's called on the syscall path when stack movement is forbidden.
//
//go:nosplit
func traceRelease( traceLocker) {
if .mp.trace.reentered > 0 {
.mp.trace.reentered--
} else {
:= .mp.trace.seqlock.Add(1)
if debugTraceReentrancy && %2 != 0 {
print("runtime: seq=", , "\n")
throw("bad use of trace.seqlock")
}
}
releasem(.mp)
}
// traceExitingSyscall marks a goroutine as exiting the syscall slow path.
//
// Must be paired with a traceExitedSyscall call.
func traceExitingSyscall() {
trace.exitingSyscall.Add(1)
}
// traceExitedSyscall marks a goroutine as having exited the syscall slow path.
func traceExitedSyscall() {
trace.exitingSyscall.Add(-1)
}
// Gomaxprocs emits a ProcsChange event.
func ( traceLocker) ( int32) {
.eventWriter(traceGoRunning, traceProcRunning).event(traceEvProcsChange, traceArg(), .stack(1))
}
// ProcStart traces a ProcStart event.
//
// Must be called with a valid P.
func ( traceLocker) () {
:= .mp.p.ptr()
// Procs are typically started within the scheduler when there is no user goroutine. If there is a user goroutine,
// it must be in _Gsyscall because the only time a goroutine is allowed to have its Proc moved around from under it
// is during a syscall.
.eventWriter(traceGoSyscall, traceProcIdle).event(traceEvProcStart, traceArg(.id), .trace.nextSeq(.gen))
}
// ProcStop traces a ProcStop event.
func ( traceLocker) ( *p) {
// The only time a goroutine is allowed to have its Proc moved around
// from under it is during a syscall.
.eventWriter(traceGoSyscall, traceProcRunning).event(traceEvProcStop)
}
// GCActive traces a GCActive event.
//
// Must be emitted by an actively running goroutine on an active P. This restriction can be changed
// easily and only depends on where it's currently called.
func ( traceLocker) () {
.eventWriter(traceGoRunning, traceProcRunning).event(traceEvGCActive, traceArg(trace.seqGC))
// N.B. Only one GC can be running at a time, so this is naturally
// serialized by the caller.
trace.seqGC++
}
// GCStart traces a GCBegin event.
//
// Must be emitted by an actively running goroutine on an active P. This restriction can be changed
// easily and only depends on where it's currently called.
func ( traceLocker) () {
.eventWriter(traceGoRunning, traceProcRunning).event(traceEvGCBegin, traceArg(trace.seqGC), .stack(3))
// N.B. Only one GC can be running at a time, so this is naturally
// serialized by the caller.
trace.seqGC++
}
// GCDone traces a GCEnd event.
//
// Must be emitted by an actively running goroutine on an active P. This restriction can be changed
// easily and only depends on where it's currently called.
func ( traceLocker) () {
.eventWriter(traceGoRunning, traceProcRunning).event(traceEvGCEnd, traceArg(trace.seqGC))
// N.B. Only one GC can be running at a time, so this is naturally
// serialized by the caller.
trace.seqGC++
}
// STWStart traces a STWBegin event.
func ( traceLocker) ( stwReason) {
// Although the current P may be in _Pgcstop here, we model the P as running during the STW. This deviates from the
// runtime's state tracking, but it's more accurate and doesn't result in any loss of information.
.eventWriter(traceGoRunning, traceProcRunning).event(traceEvSTWBegin, .string(.String()), .stack(2))
}
// STWDone traces a STWEnd event.
func ( traceLocker) () {
// Although the current P may be in _Pgcstop here, we model the P as running during the STW. This deviates from the
// runtime's state tracking, but it's more accurate and doesn't result in any loss of information.
.eventWriter(traceGoRunning, traceProcRunning).event(traceEvSTWEnd)
}
// GCSweepStart prepares to trace a sweep loop. This does not
// emit any events until traceGCSweepSpan is called.
//
// GCSweepStart must be paired with traceGCSweepDone and there
// must be no preemption points between these two calls.
//
// Must be called with a valid P.
func ( traceLocker) () {
// Delay the actual GCSweepBegin event until the first span
// sweep. If we don't sweep anything, don't emit any events.
:= .mp.p.ptr()
if .trace.maySweep {
throw("double traceGCSweepStart")
}
.trace.maySweep, .trace.swept, .trace.reclaimed = true, 0, 0
}
// GCSweepSpan traces the sweep of a single span. If this is
// the first span swept since traceGCSweepStart was called, this
// will emit a GCSweepBegin event.
//
// This may be called outside a traceGCSweepStart/traceGCSweepDone
// pair; however, it will not emit any trace events in this case.
//
// Must be called with a valid P.
func ( traceLocker) ( uintptr) {
:= .mp.p.ptr()
if .trace.maySweep {
if .trace.swept == 0 {
.eventWriter(traceGoRunning, traceProcRunning).event(traceEvGCSweepBegin, .stack(1))
.trace.inSweep = true
}
.trace.swept +=
}
}
// GCSweepDone finishes tracing a sweep loop. If any memory was
// swept (i.e. traceGCSweepSpan emitted an event) then this will emit
// a GCSweepEnd event.
//
// Must be called with a valid P.
func ( traceLocker) () {
:= .mp.p.ptr()
if !.trace.maySweep {
throw("missing traceGCSweepStart")
}
if .trace.inSweep {
.eventWriter(traceGoRunning, traceProcRunning).event(traceEvGCSweepEnd, traceArg(.trace.swept), traceArg(.trace.reclaimed))
.trace.inSweep = false
}
.trace.maySweep = false
}
// GCMarkAssistStart emits a MarkAssistBegin event.
func ( traceLocker) () {
.eventWriter(traceGoRunning, traceProcRunning).event(traceEvGCMarkAssistBegin, .stack(1))
}
// GCMarkAssistDone emits a MarkAssistEnd event.
func ( traceLocker) () {
.eventWriter(traceGoRunning, traceProcRunning).event(traceEvGCMarkAssistEnd)
}
// GoCreate emits a GoCreate event.
func ( traceLocker) ( *g, uintptr, bool) {
.trace.setStatusTraced(.gen)
:= traceEvGoCreate
if {
= traceEvGoCreateBlocked
}
.eventWriter(traceGoRunning, traceProcRunning).event(, traceArg(.goid), .startPC(), .stack(2))
}
// GoStart emits a GoStart event.
//
// Must be called with a valid P.
func ( traceLocker) () {
:= getg().m.curg
:= .m.p
:= .eventWriter(traceGoRunnable, traceProcRunning)
.event(traceEvGoStart, traceArg(.goid), .trace.nextSeq(.gen))
if .ptr().gcMarkWorkerMode != gcMarkWorkerNotWorker {
.event(traceEvGoLabel, trace.markWorkerLabels[.gen%2][.ptr().gcMarkWorkerMode])
}
}
// GoEnd emits a GoDestroy event.
//
// TODO(mknyszek): Rename this to GoDestroy.
func ( traceLocker) () {
.eventWriter(traceGoRunning, traceProcRunning).event(traceEvGoDestroy)
}
// GoSched emits a GoStop event with a GoSched reason.
func ( traceLocker) () {
.GoStop(traceGoStopGoSched)
}
// GoPreempt emits a GoStop event with a GoPreempted reason.
func ( traceLocker) () {
.GoStop(traceGoStopPreempted)
}
// GoStop emits a GoStop event with the provided reason.
func ( traceLocker) ( traceGoStopReason) {
.eventWriter(traceGoRunning, traceProcRunning).event(traceEvGoStop, traceArg(trace.goStopReasons[.gen%2][]), .stack(1))
}
// GoPark emits a GoBlock event with the provided reason.
//
// TODO(mknyszek): Replace traceBlockReason with waitReason. It's silly
// that we have both, and waitReason is way more descriptive.
func ( traceLocker) ( traceBlockReason, int) {
.eventWriter(traceGoRunning, traceProcRunning).event(traceEvGoBlock, traceArg(trace.goBlockReasons[.gen%2][]), .stack())
}
// GoUnpark emits a GoUnblock event.
func ( traceLocker) ( *g, int) {
// Emit a GoWaiting status if necessary for the unblocked goroutine.
.emitUnblockStatus(, .gen)
.eventWriter(traceGoRunning, traceProcRunning).event(traceEvGoUnblock, traceArg(.goid), .trace.nextSeq(.gen), .stack())
}
// GoSwitch emits a GoSwitch event. If destroy is true, the calling goroutine
// is simultaneously being destroyed.
func ( traceLocker) ( *g, bool) {
// Emit a GoWaiting status if necessary for the unblocked goroutine.
.emitUnblockStatus(, .gen)
:= .eventWriter(traceGoRunning, traceProcRunning)
:= traceEvGoSwitch
if {
= traceEvGoSwitchDestroy
}
.event(, traceArg(.goid), .trace.nextSeq(.gen))
}
// emitUnblockStatus emits a GoStatus GoWaiting event for a goroutine about to be
// unblocked to the trace writer.
func ( traceLocker) ( *g, uintptr) {
if !.trace.statusWasTraced() && .trace.acquireStatus() {
// TODO(go.dev/issue/65634): Although it would be nice to add a stack trace here of gp,
// we cannot safely do so. gp is in _Gwaiting and so we don't have ownership of its stack.
// We can fix this by acquiring the goroutine's scan bit.
.writer().writeGoStatus(.goid, -1, traceGoWaiting, .inMarkAssist, 0).end()
}
}
// GoSysCall emits a GoSyscallBegin event.
//
// Must be called with a valid P.
func ( traceLocker) () {
// Scribble down the M that the P is currently attached to.
:= .mp.p.ptr()
.trace.mSyscallID = int64(.mp.procid)
.eventWriter(traceGoRunning, traceProcRunning).event(traceEvGoSyscallBegin, .trace.nextSeq(.gen), .stack(1))
}
// GoSysExit emits a GoSyscallEnd event, possibly along with a GoSyscallBlocked event
// if lostP is true.
//
// lostP must be true in all cases that a goroutine loses its P during a syscall.
// This means it's not sufficient to check if it has no P. In particular, it needs to be
// true in the following cases:
// - The goroutine lost its P, it ran some other code, and then got it back. It's now running with that P.
// - The goroutine lost its P and was unable to reacquire it, and is now running without a P.
// - The goroutine lost its P and acquired a different one, and is now running with that P.
func ( traceLocker) ( bool) {
:= traceEvGoSyscallEnd
:= traceProcSyscall // Procs implicitly enter traceProcSyscall on GoSyscallBegin.
if {
= traceEvGoSyscallEndBlocked
= traceProcRunning // If a G has a P when emitting this event, it reacquired a P and is indeed running.
} else {
.mp.p.ptr().trace.mSyscallID = -1
}
.eventWriter(traceGoSyscall, ).event()
}
// ProcSteal indicates that our current M stole a P from another M.
//
// inSyscall indicates that we're stealing the P from a syscall context.
//
// The caller must have ownership of pp.
func ( traceLocker) ( *p, bool) {
// Grab the M ID we stole from.
:= .trace.mSyscallID
.trace.mSyscallID = -1
// Emit the status of the P we're stealing. We may be just about to do this when creating the event
// writer but it's not guaranteed, even if inSyscall is true. Although it might seem like from a
// syscall context we're always stealing a P for ourselves, we may have not wired it up yet (so
// it wouldn't be visible to eventWriter) or we may not even intend to wire it up to ourselves
// at all (e.g. entersyscall_gcwait).
if !.trace.statusWasTraced(.gen) && .trace.acquireStatus(.gen) {
// Careful: don't use the event writer. We never want status or in-progress events
// to trigger more in-progress events.
.writer().writeProcStatus(uint64(.id), traceProcSyscallAbandoned, .trace.inSweep).end()
}
// The status of the proc and goroutine, if we need to emit one here, is not evident from the
// context of just emitting this event alone. There are two cases. Either we're trying to steal
// the P just to get its attention (e.g. STW or sysmon retake) or we're trying to steal a P for
// ourselves specifically to keep running. The two contexts look different, but can be summarized
// fairly succinctly. In the former, we're a regular running goroutine and proc, if we have either.
// In the latter, we're a goroutine in a syscall.
:= traceGoRunning
:= traceProcRunning
if {
= traceGoSyscall
= traceProcSyscallAbandoned
}
.eventWriter(, ).event(traceEvProcSteal, traceArg(.id), .trace.nextSeq(.gen), traceArg())
}
// HeapAlloc emits a HeapAlloc event.
func ( traceLocker) ( uint64) {
.eventWriter(traceGoRunning, traceProcRunning).event(traceEvHeapAlloc, traceArg())
}
// HeapGoal reads the current heap goal and emits a HeapGoal event.
func ( traceLocker) () {
:= gcController.heapGoal()
if == ^uint64(0) {
// Heap-based triggering is disabled.
= 0
}
.eventWriter(traceGoRunning, traceProcRunning).event(traceEvHeapGoal, traceArg())
}
// GoCreateSyscall indicates that a goroutine has transitioned from dead to GoSyscall.
//
// Unlike GoCreate, the caller must be running on gp.
//
// This occurs when C code calls into Go. On pthread platforms it occurs only when
// a C thread calls into Go code for the first time.
func ( traceLocker) ( *g) {
// N.B. We should never trace a status for this goroutine (which we're currently running on),
// since we want this to appear like goroutine creation.
.trace.setStatusTraced(.gen)
.eventWriter(traceGoBad, traceProcBad).event(traceEvGoCreateSyscall, traceArg(.goid))
}
// GoDestroySyscall indicates that a goroutine has transitioned from GoSyscall to dead.
//
// Must not have a P.
//
// This occurs when Go code returns back to C. On pthread platforms it occurs only when
// the C thread is destroyed.
func ( traceLocker) () {
// N.B. If we trace a status here, we must never have a P, and we must be on a goroutine
// that is in the syscall state.
.eventWriter(traceGoSyscall, traceProcBad).event(traceEvGoDestroySyscall)
}
// To access runtime functions from runtime/trace.
// See runtime/trace/annotation.go
// trace_userTaskCreate emits a UserTaskCreate event.
//
//go:linkname trace_userTaskCreate runtime/trace.userTaskCreate
func trace_userTaskCreate(, uint64, string) {
:= traceAcquire()
if !.ok() {
// Need to do this check because the caller won't have it.
return
}
.eventWriter(traceGoRunning, traceProcRunning).event(traceEvUserTaskBegin, traceArg(), traceArg(), .string(), .stack(3))
traceRelease()
}
// trace_userTaskEnd emits a UserTaskEnd event.
//
//go:linkname trace_userTaskEnd runtime/trace.userTaskEnd
func trace_userTaskEnd( uint64) {
:= traceAcquire()
if !.ok() {
// Need to do this check because the caller won't have it.
return
}
.eventWriter(traceGoRunning, traceProcRunning).event(traceEvUserTaskEnd, traceArg(), .stack(2))
traceRelease()
}
// trace_userRegion emits a UserRegionBegin or UserRegionEnd event,
// depending on mode (0 == Begin, 1 == End).
//
// TODO(mknyszek): Just make this two functions.
//
//go:linkname trace_userRegion runtime/trace.userRegion
func trace_userRegion(, uint64, string) {
:= traceAcquire()
if !.ok() {
// Need to do this check because the caller won't have it.
return
}
var traceEv
switch {
case 0:
= traceEvUserRegionBegin
case 1:
= traceEvUserRegionEnd
default:
return
}
.eventWriter(traceGoRunning, traceProcRunning).event(, traceArg(), .string(), .stack(3))
traceRelease()
}
// trace_userLog emits a UserRegionBegin or UserRegionEnd event.
//
//go:linkname trace_userLog runtime/trace.userLog
func trace_userLog( uint64, , string) {
:= traceAcquire()
if !.ok() {
// Need to do this check because the caller won't have it.
return
}
.eventWriter(traceGoRunning, traceProcRunning).event(traceEvUserLog, traceArg(), .string(), .uniqueString(), .stack(3))
traceRelease()
}
// traceThreadDestroy is called when a thread is removed from
// sched.freem.
//
// mp must not be able to emit trace events anymore.
//
// sched.lock must be held to synchronize with traceAdvance.
func traceThreadDestroy( *m) {
assertLockHeld(&sched.lock)
// Flush all outstanding buffers to maintain the invariant
// that an M only has active buffers while on sched.freem
// or allm.
//
// Perform a traceAcquire/traceRelease on behalf of mp to
// synchronize with the tracer trying to flush our buffer
// as well.
:= .trace.seqlock.Add(1)
if debugTraceReentrancy && %2 != 1 {
throw("bad use of trace.seqlock")
}
systemstack(func() {
lock(&trace.lock)
for := range .trace.buf {
for , := range .trace.buf[] {
if != nil {
// N.B. traceBufFlush accepts a generation, but it
// really just cares about gen%2.
traceBufFlush(, uintptr())
.trace.buf[][] = nil
}
}
}
unlock(&trace.lock)
})
:= .trace.seqlock.Add(1)
if != +1 {
print("runtime: seq1=", , "\n")
throw("bad use of trace.seqlock")
}
}
The pages are generated with Golds v0.7.3. (GOOS=linux GOARCH=amd64) Golds is a Go 101 project developed by Tapir Liu. PR and bug reports are welcome and can be submitted to the issue list. Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds. |