diff --git a/api/driver.go b/api/driver.go index 41fa379..1d9a005 100644 --- a/api/driver.go +++ b/api/driver.go @@ -145,14 +145,19 @@ func (d *driverImpl) doOneFeedInTask(task *feedInTask) bool { panic("CGRA cannot handle the data rate") } - core.Trace("DataFlow", - "Behavior", "FeedIn", - slog.Float64("Time", float64(d.Engine.CurrentTime()*1e9)), - "Data", task.data[dataIndex], - "Color", task.color, - "From", port.Name(), - "To", task.remotePorts[i], - ) + timeValue := float64(d.Engine.CurrentTime() * 1e9) + if core.TraceEnabled() { + core.Trace("DataFlow", + "Behavior", "FeedIn", + slog.Float64("Time", timeValue), + "Data", task.data[dataIndex], + "Color", task.color, + "From", port.Name(), + "To", task.remotePorts[i], + ) + } else { + core.ObserveDataFlow("FeedIn", timeValue, port.Name(), string(task.remotePorts[i]), "", "") + } task.portRounds[i]++ madeProgress = true } @@ -202,15 +207,20 @@ func (d *driverImpl) doOneCollectTask(task *collectTask) bool { } task.data[dataIndex] = msg.Data.First() - core.Trace("DataFlow", - "Behavior", "Collect", - slog.Float64("Time", float64(d.Engine.CurrentTime()*1e9)), - "Data", msg.Data.First(), - "Pred", msg.Data.Pred, - "Color", task.color, - "From", task.ports[i].Name(), - "To", "None", - ) + timeValue := float64(d.Engine.CurrentTime() * 1e9) + if core.TraceEnabled() { + core.Trace("DataFlow", + "Behavior", "Collect", + slog.Float64("Time", timeValue), + "Data", msg.Data.First(), + "Pred", msg.Data.Pred, + "Color", task.color, + "From", task.ports[i].Name(), + "To", "None", + ) + } else { + core.ObserveDataFlow("Collect", timeValue, task.ports[i].Name(), "None", "", "") + } task.portRounds[i]++ madeProgress = true diff --git a/config/config.go b/config/config.go index 875f499..5c04350 100644 --- a/config/config.go +++ b/config/config.go @@ -20,9 +20,10 @@ type DeviceBuilder struct { freq sim.Freq monitor *monitoring.Monitor //portFactory portFactory - width, height int - memoryMode string // simple or shared or local - memoryShare map[[2]int]int //map[[x, y]]GroupID + width, height int + memoryMode string // simple or shared or local + memoryShare map[[2]int]int //map[[x, y]]GroupID + executionPolicy string } // type portFactory interface { @@ -74,6 +75,12 @@ func (d DeviceBuilder) WithMemoryShare(share map[[2]int]int) DeviceBuilder { return d } +// WithExecutionPolicy sets core execution policy. +func (d DeviceBuilder) WithExecutionPolicy(policy string) DeviceBuilder { + d.executionPolicy = policy + return d +} + // Build creates a CGRA device. func (d DeviceBuilder) Build(name string) cgra.Device { dev := &device{ @@ -188,6 +195,7 @@ func (d DeviceBuilder) createTiles( WithExitAddr(&exit). WithRetValAddr(&retVal). WithExitReqAddr(&exitReqTimestamp). + WithExecutionPolicy(d.executionPolicy). Build(coreName) if d.monitor != nil { diff --git a/core/builder.go b/core/builder.go index e6c3a9d..a2d4c09 100644 --- a/core/builder.go +++ b/core/builder.go @@ -7,11 +7,12 @@ import ( // Builder can create new cores. type Builder struct { - engine sim.Engine - freq sim.Freq - exitAddr *bool - retValAddr *uint32 - exitReqAddr *float64 + engine sim.Engine + freq sim.Freq + exitAddr *bool + retValAddr *uint32 + exitReqAddr *float64 + executionPolicy string } // WithEngine sets the engine. @@ -43,6 +44,12 @@ func (b Builder) WithExitReqAddr(exitReqAddr *float64) Builder { return b } +// WithExecutionPolicy sets the execution policy for issue-time gating. +func (b Builder) WithExecutionPolicy(policy string) Builder { + b.executionPolicy = policy + return b +} + // Build creates a core. // //nolint:funlen @@ -51,7 +58,8 @@ func (b Builder) Build(name string) *Core { c.TickingComponent = sim.NewTickingComponent(name, b.engine, b.freq, c) c.emu = instEmulator{ - CareFlags: true, + CareFlags: true, + ExecutionPolicy: normalizeExecutionPolicyString(b.executionPolicy), } c.state = coreState{ exit: b.exitAddr, @@ -80,6 +88,7 @@ func (b Builder) Build(name string) *Core { IsToWriteMemory: false, States: make(map[string]interface{}), Mode: SyncOp, + CurrentCycle: 0, CurrReservationState: ReservationState{ ReservationMap: make(map[int]bool), OpToExec: 0, diff --git a/core/core.go b/core/core.go index 8b32db5..ffc7369 100644 --- a/core/core.go +++ b/core/core.go @@ -59,14 +59,19 @@ func (c *Core) WriteMemory(x int, y int, data uint32, baseAddr uint32) { if x == int(c.state.TileX) && y == int(c.state.TileY) { c.state.Memory[baseAddr] = data //fmt.Printf("Core [%d][%d] write memory[%d] = %d\n", c.state.TileX, c.state.TileY, baseAddr, c.state.Memory[baseAddr]) - Trace("Memory", - "Behavior", "WriteMemory", - "Time", float64(c.Engine.CurrentTime()*1e9), - "Data", data, - "X", x, - "Y", y, - "Addr", baseAddr, - ) + timeValue := float64(c.Engine.CurrentTime() * 1e9) + if TraceEnabled() { + Trace("Memory", + "Behavior", "WriteMemory", + "Time", timeValue, + "Data", data, + "X", x, + "Y", y, + "Addr", baseAddr, + ) + } else { + ObserveMemory("WriteMemory", timeValue, x, y, "", "") + } } else { panic(fmt.Sprintf("Invalid Tile: Expect (%d, %d),but get (%d, %d)", c.state.TileX, c.state.TileY, x, y)) } @@ -85,6 +90,7 @@ func (c *Core) MapProgram(program interface{}, x int, y int) { panic("MapProgram expects core.Program type") } c.state.PCInBlock = -1 + c.state.CurrentCycle = 0 c.state.TileX = uint32(x) c.state.TileY = uint32(y) } @@ -96,6 +102,7 @@ func (c *Core) Tick() (madeProgress bool) { // madeProgress = c.emu.runRoutingRules(&c.state) || madeProgress madeProgress = c.runProgram() || madeProgress madeProgress = c.doSend() || madeProgress + c.state.CurrentCycle++ return madeProgress } @@ -103,6 +110,7 @@ func makeBytesFromUint32(data uint32) []byte { return []byte{byte(data >> 24), byte(data >> 16), byte(data >> 8), byte(data)} } +//nolint:gocyclo func (c *Core) doSend() bool { madeProgress := false for i := 0; i < 8; i++ { // only 8 directions @@ -127,15 +135,20 @@ func (c *Core) doSend() bool { continue } - Trace("DataFlow", - "Behavior", "Send", - slog.Float64("Time", float64(c.Engine.CurrentTime()*1e9)), - "Data", msg.Data.First(), - "Pred", c.state.SendBufHead[color][i].Pred, - "Color", color, - "Src", msg.Src, - "Dst", msg.Dst, - ) + timeValue := float64(c.Engine.CurrentTime() * 1e9) + if TraceEnabled() { + Trace("DataFlow", + "Behavior", "Send", + slog.Float64("Time", timeValue), + "Data", msg.Data.First(), + "Pred", c.state.SendBufHead[color][i].Pred, + "Color", color, + "Src", msg.Src, + "Dst", msg.Dst, + ) + } else { + ObserveDataFlow("Send", timeValue, "", "", string(msg.Src), string(msg.Dst)) + } c.state.SendBufHeadBusy[color][i] = false } } @@ -156,15 +169,20 @@ func (c *Core) doSend() bool { return madeProgress } - Trace("Memory", - "Behavior", "Send", - slog.Float64("Time", float64(c.Engine.CurrentTime()*1e9)), - "Data", c.state.SendBufHead[c.emu.getColorIndex("R")][cgra.Router].First(), - "Pred", c.state.SendBufHead[c.emu.getColorIndex("R")][cgra.Router].Pred, - "Color", "R", - "Src", msg.Src, - "Dst", msg.Dst, - ) + timeValue := float64(c.Engine.CurrentTime() * 1e9) + if TraceEnabled() { + Trace("Memory", + "Behavior", "Send", + slog.Float64("Time", timeValue), + "Data", c.state.SendBufHead[c.emu.getColorIndex("R")][cgra.Router].First(), + "Pred", c.state.SendBufHead[c.emu.getColorIndex("R")][cgra.Router].Pred, + "Color", "R", + "Src", msg.Src, + "Dst", msg.Dst, + ) + } else { + ObserveMemory("Send", timeValue, int(c.state.TileX), int(c.state.TileY), string(msg.Src), string(msg.Dst)) + } c.state.SendBufHeadBusy[c.emu.getColorIndex("R")][cgra.Router] = false } else { msg := mem.ReadReqBuilder{}. @@ -179,14 +197,19 @@ func (c *Core) doSend() bool { return madeProgress } - Trace("Memory", - "Behavior", "Send", - slog.Float64("Time", float64(c.Engine.CurrentTime()*1e9)), - "Data", c.state.AddrBuf, - "Color", "R", - "Src", msg.Src, - "Dst", msg.Dst, - ) + timeValue := float64(c.Engine.CurrentTime() * 1e9) + if TraceEnabled() { + Trace("Memory", + "Behavior", "Send", + slog.Float64("Time", timeValue), + "Data", c.state.AddrBuf, + "Color", "R", + "Src", msg.Src, + "Dst", msg.Dst, + ) + } else { + ObserveMemory("Send", timeValue, int(c.state.TileX), int(c.state.TileY), string(msg.Src), string(msg.Dst)) + } c.state.SendBufHeadBusy[c.emu.getColorIndex("R")][cgra.Router] = false } } @@ -198,6 +221,7 @@ func convert4BytesToUint32(data []byte) uint32 { return uint32(data[0])<<24 | uint32(data[1])<<16 | uint32(data[2])<<8 | uint32(data[3]) } +//nolint:gocyclo func (c *Core) doRecv() bool { madeProgress := false for i := 0; i < 8; i++ { //direction @@ -226,15 +250,20 @@ func (c *Core) doRecv() bool { c.state.RecvBufHeadReady[color][i] = true c.state.RecvBufHead[color][i] = msg.Data - Trace("DataFlow", - "Behavior", "Recv", - "Time", float64(c.Engine.CurrentTime()*1e9), - "Data", msg.Data.First(), - "Pred", c.state.RecvBufHead[color][i].Pred, - "Src", msg.Src, - "Dst", msg.Dst, - "Color", color, - ) + timeValue := float64(c.Engine.CurrentTime() * 1e9) + if TraceEnabled() { + Trace("DataFlow", + "Behavior", "Recv", + "Time", timeValue, + "Data", msg.Data.First(), + "Pred", c.state.RecvBufHead[color][i].Pred, + "Src", msg.Src, + "Dst", msg.Dst, + "Color", color, + ) + } else { + ObserveDataFlow("Recv", timeValue, "", "", string(msg.Src), string(msg.Dst)) + } c.ports[cgra.Side(i)].local.RetrieveIncoming() madeProgress = true @@ -254,15 +283,20 @@ func (c *Core) doRecv() bool { c.state.RecvBufHeadReady[c.emu.getColorIndex("R")][cgra.Router] = true c.state.RecvBufHead[c.emu.getColorIndex("R")][cgra.Router] = cgra.NewScalar(convert4BytesToUint32(msg.Data)) - Trace("Memory", - "Behavior", "Recv", - "Time", float64(c.Engine.CurrentTime()*1e9), - "Data", msg.Data, - "Src", msg.Src, - "Dst", msg.Dst, - "Pred", c.state.RecvBufHead[c.emu.getColorIndex("R")][cgra.Router].Pred, - "Color", "R", - ) + timeValue := float64(c.Engine.CurrentTime() * 1e9) + if TraceEnabled() { + Trace("Memory", + "Behavior", "Recv", + "Time", timeValue, + "Data", msg.Data, + "Src", msg.Src, + "Dst", msg.Dst, + "Pred", c.state.RecvBufHead[c.emu.getColorIndex("R")][cgra.Router].Pred, + "Color", "R", + ) + } else { + ObserveMemory("Recv", timeValue, int(c.state.TileX), int(c.state.TileY), string(msg.Src), string(msg.Dst)) + } c.ports[cgra.Router].local.RetrieveIncoming() madeProgress = true @@ -270,14 +304,19 @@ func (c *Core) doRecv() bool { c.state.RecvBufHeadReady[c.emu.getColorIndex("R")][cgra.Router] = true c.state.RecvBufHead[c.emu.getColorIndex("R")][cgra.Router] = cgra.NewScalar(0) - Trace("Memory", - "Behavior", "Recv", - "Time", float64(c.Engine.CurrentTime()*1e9), - "Src", msg.Src, - "Dst", msg.Dst, - "Pred", c.state.RecvBufHead[c.emu.getColorIndex("R")][cgra.Router].Pred, - "Color", "R", - ) + timeValue := float64(c.Engine.CurrentTime() * 1e9) + if TraceEnabled() { + Trace("Memory", + "Behavior", "Recv", + "Time", timeValue, + "Src", msg.Src, + "Dst", msg.Dst, + "Pred", c.state.RecvBufHead[c.emu.getColorIndex("R")][cgra.Router].Pred, + "Color", "R", + ) + } else { + ObserveMemory("Recv", timeValue, int(c.state.TileX), int(c.state.TileY), string(msg.Src), string(msg.Dst)) + } c.ports[cgra.Router].local.RetrieveIncoming() madeProgress = true diff --git a/core/emu.go b/core/emu.go index dfda1e9..ca2e7c9 100644 --- a/core/emu.go +++ b/core/emu.go @@ -24,6 +24,12 @@ const ( AsyncOp ) +const ( + ExecutionPolicyStrictTimed = "strict_timed" + ExecutionPolicyElasticScheduled = "elastic_scheduled" + ExecutionPolicyInOrderDataflow = "in_order_dataflow" +) + type routingRule struct { src cgra.Side dst cgra.Side @@ -80,7 +86,7 @@ func (r *ReservationState) SetReservationMap(ig InstructionGroup, state *coreSta r.ReservationMap[i] = true } r.OpToExec = len(ig.Operations) - print("SetReservationMap: ", r.OpToExec, "\n") + // print("SetReservationMap: ", r.OpToExec, "\n") } type coreState struct { @@ -112,10 +118,115 @@ type coreState struct { routingRules []*routingRule triggers []*Trigger CurrentTime float64 // current simulation time for logging + CurrentCycle int64 } type instEmulator struct { - CareFlags bool + CareFlags bool + ExecutionPolicy string +} + +func normalizeExecutionPolicyString(policy string) string { + text := strings.ToLower(strings.TrimSpace(policy)) + switch text { + case ExecutionPolicyStrictTimed, "strict-timed", "static": + return ExecutionPolicyStrictTimed + case ExecutionPolicyElasticScheduled, "elastic-scheduled", "hybrid": + return ExecutionPolicyElasticScheduled + case "", ExecutionPolicyInOrderDataflow, "in-order-dataflow", "dynamic": + return ExecutionPolicyInOrderDataflow + default: + // Fall back to in-order dataflow for backward compatibility. + return ExecutionPolicyInOrderDataflow + } +} + +func (i instEmulator) panicSynchronizationViolation(operation Operation, state *coreState, reason string) { + currentStep, targetStep, ii := i.resolveScheduleStep(operation, state) + panic(fmt.Sprintf( + "synchronization violation under %s: op=%s id=%d cycle=%d schedule_step=%d target_step=%d ii=%d raw_timestep=%d tile=(%d,%d): %s", + normalizeExecutionPolicyString(i.ExecutionPolicy), + operation.OpCode, + operation.ID, + state.CurrentCycle, + currentStep, + targetStep, + ii, + operation.TimeStep, + state.TileX, + state.TileY, + reason, + )) +} + +func (i instEmulator) resolveScheduleStep(operation Operation, state *coreState) (currentStep int64, targetStep int64, ii int64) { + ii = int64(state.Code.CompiledII) + if ii <= 0 { + return state.CurrentCycle, int64(operation.TimeStep), 0 + } + + currentStep = state.CurrentCycle % ii + if currentStep < 0 { + currentStep += ii + } + + targetStep = int64(operation.TimeStep) + if targetStep < 0 { + panic(fmt.Sprintf( + "invalid time_step=%d for compiled_ii=%d at op=%s id=%d tile=(%d,%d)", + operation.TimeStep, + state.Code.CompiledII, + operation.OpCode, + operation.ID, + state.TileX, + state.TileY, + )) + } + // Normalize to phase within II: compiler may emit time_step >= ii (e.g. 4 when ii=4 → step 0). + if targetStep >= ii { + targetStep = targetStep % ii + } + + return currentStep, targetStep, ii +} + +func (i instEmulator) canIssue(operation Operation, state *coreState) bool { + if !i.CareFlags || operation.InvalidIterations > 0 { + return true + } + + ready := i.CheckFlags(operation, state) + currentStep, targetStep, ii := i.resolveScheduleStep(operation, state) + + // No schedule (compiled_ii missing or 0): ignore time gating so existing workloads + // (e.g. histogram) that do not use II-based scheduling still run like in-order. + if ii <= 0 { + return ready + } + + switch normalizeExecutionPolicyString(i.ExecutionPolicy) { + case ExecutionPolicyStrictTimed: + if currentStep < targetStep { + return false + } + if currentStep == targetStep { + if ready { + return true + } + i.panicSynchronizationViolation(operation, state, "operand/credit not ready at scheduled step") + } + i.panicSynchronizationViolation(operation, state, "operation missed its exact scheduled step") + return false + case ExecutionPolicyElasticScheduled: + if currentStep < targetStep { + return false + } + return ready + case ExecutionPolicyInOrderDataflow: + return ready + default: + return ready + } } // set up the necessary state for the instruction group @@ -134,7 +245,9 @@ func (i instEmulator) SetUpInstructionGroup(index int32, state *coreState) { func (i instEmulator) RunInstructionGroup(cinst InstructionGroup, state *coreState, time float64) bool { // check the Return signal if *state.exit && time > *state.requestExitTimestamp { - fmt.Println("Exit signal ( requested at", *state.requestExitTimestamp, ") received at time", time) + if DebugEnabled() { + slog.Debug("ExitSignal", "requestedAt", *state.requestExitTimestamp, "time", time) + } return false } prevPC := state.PCInBlock @@ -173,18 +286,18 @@ func (i instEmulator) RunInstructionGroup(cinst InstructionGroup, state *coreSta } else if state.Mode == SyncOp { if progressSync { if state.NextPCInBlock == -1 { - print("PC+4 for PC=", state.PCInBlock, " X:", state.TileX, " Y:", state.TileY, "\n") - print("Instruction at PC=", state.PCInBlock, " is ", state.SelectedBlock.InstructionGroups[state.PCInBlock].Operations[0].OpCode, "\n") + // print("PC+4 for PC=", state.PCInBlock, " X:", state.TileX, " Y:", state.TileY, "\n") + // print("Instruction at PC=", state.PCInBlock, " is ", state.SelectedBlock.InstructionGroups[state.PCInBlock].Operations[0].OpCode, "\n") state.PCInBlock++ } else { - print("PC+Jump to ", state.NextPCInBlock, " X:", state.TileX, " Y:", state.TileY, "\n") + // print("PC+Jump to ", state.NextPCInBlock, " X:", state.TileX, " Y:", state.TileY, "\n") state.PCInBlock = state.NextPCInBlock } } if state.SelectedBlock != nil && state.PCInBlock >= int32(len(state.SelectedBlock.InstructionGroups)) { state.PCInBlock = -1 state.SelectedBlock = nil - print("PCInBlock = -1 at (", state.TileX, ",", state.TileY, ")\n") + // print("PCInBlock = -1 at (", state.TileX, ",", state.TileY, ")\n") slog.Info("Flow", "PCInBlock", "-1", "X", state.TileX, "Y", state.TileY) } state.NextPCInBlock = -1 @@ -210,7 +323,7 @@ func (i instEmulator) RunInstructionGroup(cinst InstructionGroup, state *coreSta func (i instEmulator) RunInstructionGroupWithSyncOps(cinst InstructionGroup, state *coreState, time float64) bool { run := true for _, operation := range cinst.Operations { - if (!i.CareFlags) || operation.InvalidIterations > 0 || i.CheckFlags(operation, state) { + if i.canIssue(operation, state) { continue } else { run = false @@ -218,27 +331,21 @@ func (i instEmulator) RunInstructionGroupWithSyncOps(cinst InstructionGroup, sta } } if run { - // Collect all results first - allResults := make(map[Operand]cgra.Data) for index := range cinst.Operations { // Get reference to the original operation in state.SelectedBlock operation := &state.SelectedBlock.InstructionGroups[state.PCInBlock].Operations[index] // Decrement InvalidIterations before running if needed if operation.InvalidIterations > 0 { - print("Invalid iteration for ", operation.OpCode, "@(", state.TileX, ",", state.TileY, ")\n") + // print("Invalid iteration for ", operation.OpCode, "@(", state.TileX, ",", state.TileY, ")\n") operation.InvalidIterations-- continue } results := i.RunOperation(*operation, state, time) - // Merge results into allResults + // In Sync mode, apply each op result immediately so later ops in the + // same instruction group can observe updated registers/ports. for operand, value := range results { - allResults[operand] = value + i.writeOperand(operand, value, state) } - //print("RunOperation", operation.OpCode, "@(", state.TileX, ",", state.TileY, ")", time, ":", "YES", "\n") - } - // Write all results at once - for operand, value := range allResults { - i.writeOperand(operand, value, state) } } return run @@ -254,12 +361,12 @@ func (i instEmulator) RunInstructionGroupWithAsyncOps(cinst InstructionGroup, st } // Get reference to the original operation in state.SelectedBlock operation := &state.SelectedBlock.InstructionGroups[state.PCInBlock].Operations[index] - if (!i.CareFlags) || operation.InvalidIterations > 0 || i.CheckFlags(*operation, state) { // can also only choose one (another pattern) + if i.canIssue(*operation, state) { // can also only choose one (another pattern) state.CurrReservationState.ReservationMap[index] = false state.CurrReservationState.OpToExec-- // Decrement InvalidIterations before running if needed if operation.InvalidIterations > 0 { - print("Invalid iteration for ", operation.OpCode, "@(", state.TileX, ",", state.TileY, ")\n") + // print("Invalid iteration for ", operation.OpCode, "@(", state.TileX, ",", state.TileY, ")\n") operation.InvalidIterations-- continue } @@ -320,7 +427,7 @@ func (i instEmulator) CheckFlags(inst Operation, state *coreState) bool { } if state.States[stateKey] == nil || state.States[stateKey] == false { // first execution if len(inst.SrcOperands.Operands) > 1 { - fmt.Println("ID", inst.ID, "bypass check") + // fmt.Println("ID", inst.ID, "bypass check") continue } else { panic("PHI_CONST or PHI_START must have two sources") @@ -341,13 +448,25 @@ func (i instEmulator) CheckFlags(inst Operation, state *coreState) bool { dstImpl := i.normalizeDirection(dst.Impl) if state.Directions[dstImpl] { if state.SendBufHeadBusy[i.getColorIndex(dst.Color)][i.getDirecIndex(dstImpl)] { + Trace( + "Backpressure", + "Time", state.CurrentTime, + "X", state.TileX, + "Y", state.TileY, + "OpCode", inst.OpCode, + "ID", inst.ID, + "Reason", "SendBufBusy", + "DstDir", dstImpl, + "Color", dst.Color, + "Policy", normalizeExecutionPolicyString(i.ExecutionPolicy), + ) flag = false break } } } //fmt.Println("[CheckFlags] checking flags for inst", inst.OpCode, "@(", state.TileX, ",", state.TileY, "):", flag) - fmt.Println("Check", inst.OpCode, "ID", inst.ID, "@(", state.TileX, ",", state.TileY, "):", flag) + // fmt.Println("Check", inst.OpCode, "ID", inst.ID, "@(", state.TileX, ",", state.TileY, "):", flag) return flag } @@ -682,9 +801,19 @@ func (i instEmulator) runLoadDirect(inst Operation, state *coreState) map[Operan src1 := inst.SrcOperands.Operands[0] addrStruct := i.readOperand(src1, state) addr := addrStruct.First() + finalPred := addrStruct.Pred + results := make(map[Operand]cgra.Data) + + // Predicated-off load should not touch memory or trigger bounds checks. + if !finalPred { + for _, dst := range inst.DstOperands.Operands { + results[dst] = cgra.NewScalarWithPred(0, false) + } + return results + } if addr >= uint32(len(state.Memory)) { - panic("memory address out of bounds") + panic("memory address out of bounds, addr: " + strconv.Itoa(int(addr)) + ", len(state.Memory): " + strconv.Itoa(len(state.Memory))) } value := state.Memory[addr] slog.Warn("Memory", @@ -695,8 +824,6 @@ func (i instEmulator) runLoadDirect(inst Operation, state *coreState) map[Operan "X", state.TileX, "Y", state.TileY, ) - finalPred := addrStruct.Pred - results := make(map[Operand]cgra.Data) for _, dst := range inst.DstOperands.Operands { results[dst] = cgra.NewScalarWithPred(value, finalPred) } @@ -754,6 +881,11 @@ func (i instEmulator) runStoreDirect(inst Operation, state *coreState) map[Opera src2 := inst.SrcOperands.Operands[1] addrStruct := i.readOperand(src2, state) addr := addrStruct.First() + finalPred := addrStruct.Pred && valueStruct.Pred + if !finalPred { + Trace("Inst", "Time", state.CurrentTime, "OpCode", inst.OpCode, "ID", inst.ID, "X", state.TileX, "Y", state.TileY, "Pred", finalPred) + return make(map[Operand]cgra.Data) + } if addr >= uint32(len(state.Memory)) { panic("memory address out of bounds, addr: " + strconv.Itoa(int(addr)) + ", len(state.Memory): " + strconv.Itoa(len(state.Memory))) } @@ -765,7 +897,6 @@ func (i instEmulator) runStoreDirect(inst Operation, state *coreState) map[Opera "Y", state.TileY, ) state.Memory[addr] = value - finalPred := addrStruct.Pred && valueStruct.Pred Trace("Inst", "Time", state.CurrentTime, "OpCode", inst.OpCode, "ID", inst.ID, "X", state.TileX, "Y", state.TileY, "Pred", finalPred) // elect no next PC return make(map[Operand]cgra.Data) @@ -921,7 +1052,7 @@ func (i instEmulator) runSub(inst Operation, state *coreState) map[Operand]cgra. dstValSigned := src1Signed - src2Signed dstVal := uint32(dstValSigned) - fmt.Printf("ISUB: Subtracting %d (src1) - %d (src2) = %d\n", src1Signed, src2Signed, dstValSigned) + // fmt.Printf("ISUB: Subtracting %d (src1) - %d (src2) = %d\n", src1Signed, src2Signed, dstValSigned) finalPred := src1Struct.Pred && src2Struct.Pred results := make(map[Operand]cgra.Data) @@ -973,8 +1104,9 @@ func (i instEmulator) runMulAdd(inst Operation, state *coreState) map[Operand]cg s2Val := int32(s2.First()) dstValSigned := s0Val*s1Val + s2Val dstVal := uint32(dstValSigned) - finalPred := s0.Pred && s1.Pred && s2.Pred - + //finalPred := s0.Pred && s1.Pred && s2.Pred + //Only for systolic array currently. if need for other cases, please modify the finalPred calculation. + finalPred := s0.Pred && s1.Pred results := make(map[Operand]cgra.Data) for _, dst := range inst.DstOperands.Operands { results[dst] = cgra.NewScalarWithPred(dstVal, finalPred) @@ -1201,9 +1333,9 @@ func (i instEmulator) runRetImm(inst Operation, state *coreState, time float64) *state.retVal = srcVal *state.exit = true *state.requestExitTimestamp = time - fmt.Println("++++++++++++ RETURN executed", srcVal, "T=", time) - } else { - fmt.Println("++++++++++++ RETURN bypassed") + // fmt.Println("++++++++++++ RETURN executed", srcVal, "T=", time) + // } else { + // fmt.Println("++++++++++++ RETURN bypassed") } } else { panic("RETURN_VALUE requires a source operand") @@ -1229,9 +1361,9 @@ func (i instEmulator) runRetDelay(inst Operation, state *coreState, time float64 *state.retVal = 0 *state.exit = true *state.requestExitTimestamp = time + ExitDelay - fmt.Println("++++++++++++ RETURN executed", srcVal, "T=", time) - } else { - fmt.Println("++++++++++++ RETURN bypassed") + // fmt.Println("++++++++++++ RETURN executed", srcVal, "T=", time) + // } else { + // fmt.Println("++++++++++++ RETURN bypassed") } } else { panic("RETURN_VOID requires a source operand") @@ -1338,14 +1470,14 @@ func (i instEmulator) runCmpExport(inst Operation, state *coreState) map[Operand for _, dst := range inst.DstOperands.Operands { results[dst] = cgra.NewScalarWithPred(1, finalPred) } - fmt.Println(">>>>>>>>>>>>>>> ICMP_EQ: ", src1Val.First(), src2Val.First(), "Yes") + // fmt.Println(">>>>>>>>>>>>>>> ICMP_EQ: ", src1Val.First(), src2Val.First(), "Yes") } else { finalPred = src1Val.Pred resultVal = 0 for _, dst := range inst.DstOperands.Operands { results[dst] = cgra.NewScalarWithPred(0, finalPred) } - fmt.Println(">>>>>>>>>>>>>>> ICMP_EQ: ", src1Val.First(), src2Val.First(), "No") + // fmt.Println(">>>>>>>>>>>>>>> ICMP_EQ: ", src1Val.First(), src2Val.First(), "No") } Trace("Inst", "Time", state.CurrentTime, "OpCode", inst.OpCode, "ID", inst.ID, "X", state.TileX, "Y", state.TileY, "Src1", fmt.Sprintf("%d(%t)", src1Val.First(), src1Val.Pred), "Src2", fmt.Sprintf("%d(%t)", src2Val.First(), src2Val.Pred), "Result", fmt.Sprintf("%d(%t)", resultVal, finalPred)) return results @@ -1561,7 +1693,7 @@ func (i instEmulator) runPhiStart(inst Operation, state *coreState) map[Operand] result = src1Val finalPred = src1Pred state.States[stateKey] = true - fmt.Println("set state.States[", stateKey, "] to true") + // fmt.Println("set state.States[", stateKey, "] to true") for _, dst := range inst.DstOperands.Operands { results[dst] = cgra.NewScalarWithPred(result, finalPred) } @@ -1616,7 +1748,7 @@ func (i instEmulator) runGrantPred(inst Operation, state *coreState) map[Operand results[dst] = cgra.NewScalarWithPred(srcVal, finalPred) } - fmt.Println("<<<<<<<<<<<<<< GRANTPRED: ", srcVal, predVal, finalPred) + // fmt.Println("<<<<<<<<<<<<<< GRANTPRED: ", srcVal, predVal, finalPred) Trace("Inst", "Time", state.CurrentTime, "OpCode", inst.OpCode, "ID", inst.ID, "X", state.TileX, "Y", state.TileY, "SrcOperand", fmt.Sprintf("%d(%t)", srcVal, srcStruct.Pred), "PredOperand", fmt.Sprintf("%d(%t)", predVal, predStruct.Pred), "Pred", finalPred, "Result", fmt.Sprintf("%d(%t)", srcVal, finalPred)) // elect no next PC diff --git a/core/execution_policy_test.go b/core/execution_policy_test.go new file mode 100644 index 0000000..8d8f284 --- /dev/null +++ b/core/execution_policy_test.go @@ -0,0 +1,194 @@ +package core + +import ( + "os" + "strings" + "testing" +) + +func newPolicyTestState() coreState { + state := coreState{ + Directions: map[string]bool{ + "North": true, + "East": true, + "South": true, + "West": true, + "NorthEast": true, + "SouthEast": true, + "SouthWest": true, + "NorthWest": true, + "Router": true, + }, + RecvBufHeadReady: make([][]bool, 4), + SendBufHeadBusy: make([][]bool, 4), + } + + for i := 0; i < 4; i++ { + state.RecvBufHeadReady[i] = make([]bool, 12) + state.SendBufHeadBusy[i] = make([]bool, 12) + } + + return state +} + +func TestCanIssueInOrderIgnoresTimeStep(t *testing.T) { + emu := instEmulator{ + CareFlags: true, + ExecutionPolicy: ExecutionPolicyInOrderDataflow, + } + state := newPolicyTestState() + state.CurrentCycle = 0 + op := Operation{ + OpCode: "NOP", + TimeStep: 10, + } + + if !emu.canIssue(op, &state) { + t.Fatalf("in_order_dataflow should ignore timestep and allow ready op") + } +} + +func TestCanIssueElasticScheduled(t *testing.T) { + emu := instEmulator{ + CareFlags: true, + ExecutionPolicy: ExecutionPolicyElasticScheduled, + } + state := newPolicyTestState() + state.Code.CompiledII = 10 // must have schedule so elastic time-gating applies + op := Operation{ + OpCode: "NOP", + TimeStep: 5, + } + + state.CurrentCycle = 4 + if emu.canIssue(op, &state) { + t.Fatalf("elastic_scheduled should block before timestep") + } + + state.CurrentCycle = 5 + if !emu.canIssue(op, &state) { + t.Fatalf("elastic_scheduled should allow at timestep when ready") + } +} + +func TestCanIssueElasticScheduledWithCompiledIIConversion(t *testing.T) { + emu := instEmulator{ + CareFlags: true, + ExecutionPolicy: ExecutionPolicyElasticScheduled, + } + state := newPolicyTestState() + state.Code.CompiledII = 4 + op := Operation{ + OpCode: "NOP", + TimeStep: 1, + } + + state.CurrentCycle = 0 // step 0 + if emu.canIssue(op, &state) { + t.Fatalf("elastic_scheduled should block before converted step") + } + + state.CurrentCycle = 2 // step 2 + if !emu.canIssue(op, &state) { + t.Fatalf("elastic_scheduled should allow when converted step >= time_step") + } + + state.CurrentCycle = 5 // step 1 (5 %% 4) + if !emu.canIssue(op, &state) { + t.Fatalf("elastic_scheduled should allow on converted matching step") + } +} + +func TestCanIssueStrictTimedViolation(t *testing.T) { + emu := instEmulator{ + CareFlags: true, + ExecutionPolicy: ExecutionPolicyStrictTimed, + } + state := newPolicyTestState() + state.Code.CompiledII = 4 // must have schedule so strict time check runs + state.CurrentCycle = 6 // step 2 (6%4); op was for step 1 → missed step, violation + state.RecvBufHeadReady[0][0] = true // North dir slot 0 ready so CheckFlags passes + op := Operation{ + OpCode: "DATA_MOV", + TimeStep: 1, // scheduled step 1; current step 2 > 1 → panic + SrcOperands: OperandList{ + Operands: []Operand{ + {Impl: "North", Color: "R"}, + }, + }, + } + + defer func() { + recovered := recover() + if recovered == nil { + t.Fatalf("expected strict_timed synchronization violation panic") + } + if !strings.Contains(recovered.(string), "synchronization violation") { + t.Fatalf("unexpected panic: %v", recovered) + } + }() + + _ = emu.canIssue(op, &state) +} + +func TestCanIssueStrictTimedWithCompiledIIConversion(t *testing.T) { + emu := instEmulator{ + CareFlags: true, + ExecutionPolicy: ExecutionPolicyStrictTimed, + } + state := newPolicyTestState() + state.Code.CompiledII = 4 + op := Operation{ + OpCode: "NOP", + TimeStep: 1, + } + + state.CurrentCycle = 5 // step 1 + if !emu.canIssue(op, &state) { + t.Fatalf("strict_timed should allow when converted step equals time_step") + } + + state.CurrentCycle = 6 // step 2: missed exact step + defer func() { + recovered := recover() + if recovered == nil { + t.Fatalf("expected strict_timed missed-step synchronization violation") + } + if !strings.Contains(recovered.(string), "missed its exact scheduled step") { + t.Fatalf("unexpected panic: %v", recovered) + } + }() + _ = emu.canIssue(op, &state) +} + +func TestLoadProgramFileFromYAMLPreservesTimeStep(t *testing.T) { + filePath := "../test/testbench/stonneGEMM8x8/gemm.yaml" + if _, err := os.Stat(filePath); os.IsNotExist(err) { + t.Skipf("test file does not exist: %s", filePath) + } + + programMap := LoadProgramFileFromYAML(filePath) + program, ok := programMap["(0,0)"] + if !ok { + t.Fatalf("core (0,0) not found in parsed program") + } + if len(program.EntryBlocks) == 0 || len(program.EntryBlocks[0].InstructionGroups) < 2 { + t.Fatalf("unexpected program structure for core (0,0)") + } + + group0 := program.EntryBlocks[0].InstructionGroups[0] + if len(group0.Operations) == 0 { + t.Fatalf("group0 has no operations") + } + if group0.Operations[0].TimeStep != 0 { + t.Fatalf("unexpected timestep for first op: got %d want 0", group0.Operations[0].TimeStep) + } + + group1 := program.EntryBlocks[0].InstructionGroups[1] + if len(group1.Operations) == 0 { + t.Fatalf("group1 has no operations") + } + if group1.Operations[0].TimeStep != 1 { + t.Fatalf("unexpected timestep for second group first op: got %d want 1", group1.Operations[0].TimeStep) + } +} diff --git a/core/program.go b/core/program.go index 4d1db00..c8fe37e 100644 --- a/core/program.go +++ b/core/program.go @@ -3,6 +3,7 @@ package core import ( "fmt" + "log/slog" "os" "regexp" "strconv" @@ -104,6 +105,7 @@ type Operation struct { SrcOperands OperandList ID int // ID from YAML file InvalidIterations int // Invalid iterations from YAML file + TimeStep int // Time step from YAML file } // OperandList wraps source or destination operands for an operation. @@ -135,8 +137,9 @@ func LoadProgramFileFromYAML(programFilePath string) map[string]Program { config := root.ArrayConfig - // Debug: Print the parsed config - fmt.Printf("Debug: Parsed config - Rows: %d, Cols: %d, Cores: %d\n", config.Rows, config.Cols, len(config.Cores)) + if DebugEnabled() { + slog.Debug("ParsedProgramConfig", "rows", config.Rows, "cols", config.Cols, "cores", len(config.Cores)) + } // Convert to map[(x,y)]Program programMap := make(map[string]Program) @@ -144,7 +147,9 @@ func LoadProgramFileFromYAML(programFilePath string) map[string]Program { for _, core := range config.Cores { // Create coordinate key coordKey := fmt.Sprintf("(%d,%d)", core.Column, core.Row) - fmt.Printf("Debug: Processing core at %s with %d entries\n", coordKey, len(core.Entries)) + if DebugEnabled() { + slog.Debug("ProcessingProgramCore", "coord", coordKey, "entries", len(core.Entries)) + } // Convert core entries to Program structure var entryBlocks []EntryBlock @@ -192,6 +197,7 @@ func LoadProgramFileFromYAML(programFilePath string) map[string]Program { DstOperands: OperandList{Operands: dstOperands}, ID: yamlOp.ID, InvalidIterations: yamlOp.InvalidIterations, + TimeStep: yamlOp.TimeStep, } operations = append(operations, operation) diff --git a/core/util.go b/core/util.go index 1e3644c..5f797ad 100644 --- a/core/util.go +++ b/core/util.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "log/slog" + "sync/atomic" + "time" "github.com/jedib0t/go-pretty/v6/table" ) @@ -11,15 +13,223 @@ import ( const ( // PrintToggle enables verbose state table printing in debugging. PrintToggle = false - // LevelTrace is a custom trace level above info. - LevelTrace slog.Level = slog.LevelInfo + 1 + // LevelTrace is a custom trace level below debug/info. + LevelTrace slog.Level = slog.LevelDebug - 4 ) +// TraceObservation captures the subset of a trace event needed for report generation. +type TraceObservation struct { + WallTime time.Time + Msg string + Behavior string + Time *float64 + X *int + Y *int + Src string + Dst string + From string + To string +} + +var traceEnabled atomic.Bool +var traceObserver func(TraceObservation) + +func init() { + traceEnabled.Store(true) +} + +// SetTraceEnabled controls whether trace events are written to the slog trace handler. +func SetTraceEnabled(enabled bool) { + traceEnabled.Store(enabled) +} + +// TraceEnabled reports whether trace output is enabled. +func TraceEnabled() bool { + return traceEnabled.Load() +} + +// DebugEnabled reports whether debug logging is enabled on the default logger. +func DebugEnabled() bool { + return slog.Default().Enabled(context.Background(), slog.LevelDebug) +} + +// SetTraceObserver registers a report observer for trace events. +func SetTraceObserver(observer func(TraceObservation)) { + traceObserver = observer +} + // Trace writes a trace-level structured log record. func Trace(msg string, args ...any) { + if traceObserver != nil { + if observation, valid := buildTraceObservation(msg, args...); valid { + traceObserver(observation) + } + } + if !TraceEnabled() { + return + } slog.Log(context.Background(), LevelTrace, msg, args...) } +// ObserveDataFlow records a dataflow event for report generation without emitting trace output. +func ObserveDataFlow(behavior string, timeValue float64, from, to, src, dst string) { + observeTrace(TraceObservation{ + WallTime: time.Now(), + Msg: "DataFlow", + Behavior: behavior, + Time: float64Ptr(timeValue), + From: from, + To: to, + Src: src, + Dst: dst, + }) +} + +// ObserveMemory records a memory event for report generation without emitting trace output. +func ObserveMemory(behavior string, timeValue float64, x, y int, src, dst string) { + observeTrace(TraceObservation{ + WallTime: time.Now(), + Msg: "Memory", + Behavior: behavior, + Time: float64Ptr(timeValue), + X: intPtr(x), + Y: intPtr(y), + Src: src, + Dst: dst, + }) +} + +// ObserveInst records an instruction event for report generation without emitting trace output. +func ObserveInst(timeValue float64, x, y int) { + observeTrace(TraceObservation{ + WallTime: time.Now(), + Msg: "Inst", + Time: float64Ptr(timeValue), + X: intPtr(x), + Y: intPtr(y), + }) +} + +// ObserveBackpressure records a backpressure event for report generation without emitting trace output. +func ObserveBackpressure(timeValue float64, x, y int) { + observeTrace(TraceObservation{ + WallTime: time.Now(), + Msg: "Backpressure", + Time: float64Ptr(timeValue), + X: intPtr(x), + Y: intPtr(y), + }) +} + +func observeTrace(observation TraceObservation) { + if traceObserver != nil { + traceObserver(observation) + } +} + +func buildTraceObservation(msg string, args ...any) (TraceObservation, bool) { + observation := TraceObservation{ + WallTime: time.Now(), + Msg: msg, + } + if msg != "Inst" && msg != "Memory" && msg != "DataFlow" && msg != "Backpressure" { + return observation, false + } + + for i := 0; i < len(args); i++ { + switch value := args[i].(type) { + case slog.Attr: + assignObservationField(&observation, value.Key, value.Value.Any()) + case string: + if i+1 >= len(args) { + continue + } + assignObservationField(&observation, value, args[i+1]) + i++ + } + } + + return observation, true +} + +//nolint:gocyclo +func assignObservationField(observation *TraceObservation, key string, value any) { + switch key { + case "Behavior": + observation.Behavior = fmt.Sprint(value) + case "Time": + if converted, ok := toFloat64(value); ok { + observation.Time = float64Ptr(converted) + } + case "X": + if converted, ok := toInt(value); ok { + observation.X = intPtr(converted) + } + case "Y": + if converted, ok := toInt(value); ok { + observation.Y = intPtr(converted) + } + case "Src": + observation.Src = fmt.Sprint(value) + case "Dst": + observation.Dst = fmt.Sprint(value) + case "From": + observation.From = fmt.Sprint(value) + case "To": + observation.To = fmt.Sprint(value) + } +} + +func toFloat64(value any) (float64, bool) { + switch typed := value.(type) { + case float64: + return typed, true + case float32: + return float64(typed), true + case int: + return float64(typed), true + case int64: + return float64(typed), true + case int32: + return float64(typed), true + case uint32: + return float64(typed), true + case uint64: + return float64(typed), true + default: + return 0, false + } +} + +func toInt(value any) (int, bool) { + switch typed := value.(type) { + case int: + return typed, true + case int32: + return int(typed), true + case int64: + return int(typed), true + case uint32: + return int(typed), true + case uint64: + return int(typed), true + default: + return 0, false + } +} + +func intPtr(value int) *int { + ptr := new(int) + *ptr = value + return ptr +} + +func float64Ptr(value float64) *float64 { + ptr := new(float64) + *ptr = value + return ptr +} + // PrintState prints a formatted snapshot of core runtime state. // //nolint:gocyclo,funlen diff --git a/report/report.go b/report/report.go index 5b142b0..99c8bb4 100644 --- a/report/report.go +++ b/report/report.go @@ -10,6 +10,9 @@ import ( "os" "regexp" "sort" + "time" + + "github.com/sarchlab/zeonica/core" ) // GenerateOptions controls report generation behavior from a trace log. @@ -25,22 +28,26 @@ type GenerateOptions struct { // Report is the aggregate execution summary derived from a trace log. type Report struct { - TestName string `json:"testName,omitempty"` - LogPath string `json:"logPath"` - Grid GridInfo `json:"grid"` - TotalCycles int64 `json:"totalCycles"` - ActiveCycles int64 `json:"activeCyclesGlobal"` - IdleCycles int64 `json:"idleCyclesGlobal"` - Passed *bool `json:"passed,omitempty"` - MismatchCount *int `json:"mismatchCount,omitempty"` - InstCount int64 `json:"instCount"` - SendCount int64 `json:"sendCount"` - RecvCount int64 `json:"recvCount"` - MemoryCount int64 `json:"memoryCount"` - TotalEvents int64 `json:"totalEvents"` - ActiveTileCount int `json:"activeTileCount"` - Tiles []TileStats `json:"tiles"` - TopHotTiles []TopHotTile `json:"topHotTiles"` + TestName string `json:"testName,omitempty"` + LogPath string `json:"logPath"` + Grid GridInfo `json:"grid"` + TotalCycles int64 `json:"totalCycles"` + ActiveCycles int64 `json:"activeCyclesGlobal"` + IdleCycles int64 `json:"idleCyclesGlobal"` + Passed *bool `json:"passed,omitempty"` + MismatchCount *int `json:"mismatchCount,omitempty"` + InstCount int64 `json:"instCount"` + SendCount int64 `json:"sendCount"` + RecvCount int64 `json:"recvCount"` + MemoryCount int64 `json:"memoryCount"` + TotalEvents int64 `json:"totalEvents"` + WallClockDurationSec float64 `json:"wallClockDurationSec"` + BackpressureCount int64 `json:"backpressureCount"` + BackpressureCycles int64 `json:"backpressureCycles"` + ActiveTileCount int `json:"activeTileCount"` + Tiles []TileStats `json:"tiles"` + TopHotTiles []TopHotTile `json:"topHotTiles"` + TopBackpressureTiles []TopBackpressureTile `json:"topBackpressureTiles"` } // GridInfo describes the grid size used by the workload. @@ -51,16 +58,17 @@ type GridInfo struct { // TileStats stores per-tile metrics in the generated report. type TileStats struct { - X int `json:"x"` - Y int `json:"y"` - Coord string `json:"coord"` - ActiveCycles int64 `json:"activeCycles"` - UtilizationPct float64 `json:"utilizationPct"` - InstCount int64 `json:"instCount"` - SendCount int64 `json:"sendCount"` - RecvCount int64 `json:"recvCount"` - MemoryCount int64 `json:"memoryCount"` - TotalEvents int64 `json:"totalEvents"` + X int `json:"x"` + Y int `json:"y"` + Coord string `json:"coord"` + ActiveCycles int64 `json:"activeCycles"` + UtilizationPct float64 `json:"utilizationPct"` + InstCount int64 `json:"instCount"` + SendCount int64 `json:"sendCount"` + RecvCount int64 `json:"recvCount"` + MemoryCount int64 `json:"memoryCount"` + TotalEvents int64 `json:"totalEvents"` + BackpressureCount int64 `json:"backpressureCount"` } // TopHotTile is a ranked hot tile summary entry. @@ -73,6 +81,14 @@ type TopHotTile struct { TotalEvents int64 `json:"totalEvents"` } +// TopBackpressureTile is a ranked backpressure hot tile entry. +type TopBackpressureTile struct { + X int `json:"x"` + Y int `json:"y"` + Coord string `json:"coord"` + BackpressureCount int64 `json:"backpressureCount"` +} + type traceEvent struct { Timestamp string `json:"time"` Msg string `json:"msg"` @@ -92,95 +108,156 @@ type tileCoord struct { } type tileAccumulator struct { - cycles map[int64]struct{} - instCount int64 - sendCount int64 - recvCount int64 - memoryCount int64 - totalEvents int64 + cycles map[int64]struct{} + backpressureCycles map[int64]struct{} + instCount int64 + sendCount int64 + recvCount int64 + memoryCount int64 + totalEvents int64 + backpressureCount int64 } -var tileEndpointPattern = regexp.MustCompile(`Device\.Tile\[(\d+)\]\[(\d+)\]\.Core\.`) +type collector struct { + tileData map[tileCoord]*tileAccumulator + globalCycleSet map[int64]struct{} + globalBackpressureCycles map[int64]struct{} + maxCycle int64 + maxX int + maxY int + globalBackpressureCount int64 + minWallTS *time.Time + maxWallTS *time.Time +} -// GenerateFromLog builds a report by parsing a JSON trace log. -// -//nolint:gocyclo,funlen -func GenerateFromLog(opts GenerateOptions) (Report, error) { - if opts.LogPath == "" { - return Report{}, fmt.Errorf("log path is required") - } +// Observer collects report statistics directly from runtime trace observations. +type Observer struct { + collector *collector +} - topN := opts.TopN - if topN <= 0 { - topN = 5 - } +var tileEndpointPattern = regexp.MustCompile(`Device\.Tile\[(\d+)\]\[(\d+)\]\.Core\.`) - file, err := os.Open(opts.LogPath) - if err != nil { - return Report{}, fmt.Errorf("open log file: %w", err) +// NewObserver creates a report observer for runtime trace events. +func NewObserver() *Observer { + return &Observer{ + collector: newCollector(), } - defer func() { _ = file.Close() }() +} - tileData := make(map[tileCoord]*tileAccumulator) - globalCycleSet := make(map[int64]struct{}) +func newCollector() *collector { + return &collector{ + tileData: make(map[tileCoord]*tileAccumulator), + globalCycleSet: make(map[int64]struct{}), + globalBackpressureCycles: make(map[int64]struct{}), + maxCycle: -1, + maxX: -1, + maxY: -1, + } +} - var maxCycle int64 = -1 - maxX, maxY := -1, -1 +// Observe records a runtime trace observation into the in-memory report collector. +func (o *Observer) Observe(observation core.TraceObservation) { + if o == nil || o.collector == nil { + return + } + + event := traceEvent{ + Timestamp: observation.WallTime.Format(time.RFC3339Nano), + Msg: observation.Msg, + Behavior: observation.Behavior, + Time: observation.Time, + X: observation.X, + Y: observation.Y, + Src: observation.Src, + Dst: observation.Dst, + From: observation.From, + To: observation.To, + } + o.collector.observe(event) +} - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Bytes() - if len(line) == 0 { - continue +// Build materializes a Report using the collected runtime events. +func (o *Observer) Build(opts GenerateOptions) Report { + if o == nil || o.collector == nil { + return Report{ + TestName: opts.TestName, + LogPath: opts.LogPath, + Grid: GridInfo{ + Width: opts.GridWidth, + Height: opts.GridHeight, + }, + Passed: opts.Passed, + MismatchCount: opts.MismatchCount, } + } + return o.collector.build(opts) +} - var event traceEvent - if err := json.Unmarshal(line, &event); err != nil { - continue +//nolint:gocyclo +func (c *collector) observe(event traceEvent) { + if ts, err := time.Parse(time.RFC3339Nano, event.Timestamp); err == nil { + if c.minWallTS == nil || ts.Before(*c.minWallTS) { + t := ts + c.minWallTS = &t } - - coord, ok := resolveTileCoord(event) - if !ok { - continue + if c.maxWallTS == nil || ts.After(*c.maxWallTS) { + t := ts + c.maxWallTS = &t } + } - if coord.x > maxX { - maxX = coord.x - } - if coord.y > maxY { - maxY = coord.y + coord, ok := resolveTileCoord(event) + if !ok { + return + } + + if coord.x > c.maxX { + c.maxX = coord.x + } + if coord.y > c.maxY { + c.maxY = coord.y + } + + acc, exists := c.tileData[coord] + if !exists { + acc = &tileAccumulator{ + cycles: make(map[int64]struct{}), + backpressureCycles: make(map[int64]struct{}), } + c.tileData[coord] = acc + } - acc, exists := tileData[coord] - if !exists { - acc = &tileAccumulator{ - cycles: make(map[int64]struct{}), - } - tileData[coord] = acc + isBackpressureEvent := event.Msg == "Backpressure" + cycle, hasCycle := parseCycle(event.Time) + if hasCycle && !isBackpressureEvent { + acc.cycles[cycle] = struct{}{} + c.globalCycleSet[cycle] = struct{}{} + if cycle > c.maxCycle { + c.maxCycle = cycle } + } - cycle, hasCycle := parseCycle(event.Time) + if classifyAndCount(event, acc, cycle, hasCycle) { + c.globalBackpressureCount++ if hasCycle { - acc.cycles[cycle] = struct{}{} - globalCycleSet[cycle] = struct{}{} - if cycle > maxCycle { - maxCycle = cycle - } + c.globalBackpressureCycles[cycle] = struct{}{} } - - classifyAndCount(event, acc) } +} - if err := scanner.Err(); err != nil { - return Report{}, fmt.Errorf("scan log file: %w", err) +//nolint:gocyclo,funlen +func (c *collector) build(opts GenerateOptions) Report { + topN := opts.TopN + if topN <= 0 { + topN = 5 } totalCycles := int64(0) - if maxCycle >= 0 { - totalCycles = maxCycle + 1 + if c.maxCycle >= 0 { + totalCycles = c.maxCycle + 1 } - activeCycles := int64(len(globalCycleSet)) + activeCycles := int64(len(c.globalCycleSet)) idleCycles := totalCycles - activeCycles if idleCycles < 0 { idleCycles = 0 @@ -188,11 +265,11 @@ func GenerateFromLog(opts GenerateOptions) (Report, error) { width := opts.GridWidth if width <= 0 { - width = maxX + 1 + width = c.maxX + 1 } height := opts.GridHeight if height <= 0 { - height = maxY + 1 + height = c.maxY + 1 } if width < 0 { width = 0 @@ -201,8 +278,8 @@ func GenerateFromLog(opts GenerateOptions) (Report, error) { height = 0 } - tiles := make([]TileStats, 0, len(tileData)) - for coord, acc := range tileData { + tiles := make([]TileStats, 0, len(c.tileData)) + for coord, acc := range c.tileData { activeTileCycles := int64(len(acc.cycles)) util := 0.0 if totalCycles > 0 { @@ -210,16 +287,17 @@ func GenerateFromLog(opts GenerateOptions) (Report, error) { } tiles = append(tiles, TileStats{ - X: coord.x, - Y: coord.y, - Coord: formatCoord(coord.x, coord.y), - ActiveCycles: activeTileCycles, - UtilizationPct: util, - InstCount: acc.instCount, - SendCount: acc.sendCount, - RecvCount: acc.recvCount, - MemoryCount: acc.memoryCount, - TotalEvents: acc.totalEvents, + X: coord.x, + Y: coord.y, + Coord: formatCoord(coord.x, coord.y), + ActiveCycles: activeTileCycles, + UtilizationPct: util, + InstCount: acc.instCount, + SendCount: acc.sendCount, + RecvCount: acc.recvCount, + MemoryCount: acc.memoryCount, + TotalEvents: acc.totalEvents, + BackpressureCount: acc.backpressureCount, }) } @@ -245,27 +323,75 @@ func GenerateFromLog(opts GenerateOptions) (Report, error) { } topHotTiles := buildTopHotTiles(tiles, topN) + topBackpressureTiles := buildTopBackpressureTiles(tiles, topN) + wallClockDurationSec := 0.0 + if c.minWallTS != nil && c.maxWallTS != nil { + d := c.maxWallTS.Sub(*c.minWallTS).Seconds() + if d > 0 { + wallClockDurationSec = d + } + } - report := Report{ - TestName: opts.TestName, - LogPath: opts.LogPath, - Grid: GridInfo{Width: width, Height: height}, - TotalCycles: totalCycles, - ActiveCycles: activeCycles, - IdleCycles: idleCycles, - Passed: opts.Passed, - MismatchCount: opts.MismatchCount, - InstCount: instTotal, - SendCount: sendTotal, - RecvCount: recvTotal, - MemoryCount: memoryTotal, - TotalEvents: eventTotal, - ActiveTileCount: len(tiles), - Tiles: tiles, - TopHotTiles: topHotTiles, - } - - return report, nil + return Report{ + TestName: opts.TestName, + LogPath: opts.LogPath, + Grid: GridInfo{Width: width, Height: height}, + TotalCycles: totalCycles, + ActiveCycles: activeCycles, + IdleCycles: idleCycles, + Passed: opts.Passed, + MismatchCount: opts.MismatchCount, + InstCount: instTotal, + SendCount: sendTotal, + RecvCount: recvTotal, + MemoryCount: memoryTotal, + TotalEvents: eventTotal, + WallClockDurationSec: wallClockDurationSec, + BackpressureCount: c.globalBackpressureCount, + BackpressureCycles: int64(len(c.globalBackpressureCycles)), + ActiveTileCount: len(tiles), + Tiles: tiles, + TopHotTiles: topHotTiles, + TopBackpressureTiles: topBackpressureTiles, + } +} + +// GenerateFromLog builds a report by parsing a JSON trace log. +// +//nolint:gocyclo,funlen +func GenerateFromLog(opts GenerateOptions) (Report, error) { + if opts.LogPath == "" { + return Report{}, fmt.Errorf("log path is required") + } + + file, err := os.Open(opts.LogPath) + if err != nil { + return Report{}, fmt.Errorf("open log file: %w", err) + } + defer func() { _ = file.Close() }() + + collector := newCollector() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Bytes() + if len(line) == 0 { + continue + } + + var event traceEvent + if err := json.Unmarshal(line, &event); err != nil { + continue + } + + collector.observe(event) + } + + if err := scanner.Err(); err != nil { + return Report{}, fmt.Errorf("scan log file: %w", err) + } + + return collector.build(opts), nil } // SaveJSON writes a report as pretty-printed JSON. @@ -298,6 +424,8 @@ func PrintSummaryToWriter(report Report, w io.Writer) { fmt.Fprintf(w, "cycles: total=%d active=%d idle=%d\n", report.TotalCycles, report.ActiveCycles, report.IdleCycles) fmt.Fprintf(w, "events: total=%d inst=%d send=%d recv=%d memory=%d\n", report.TotalEvents, report.InstCount, report.SendCount, report.RecvCount, report.MemoryCount) + fmt.Fprintf(w, "simulation time: wall=%.3fs\n", report.WallClockDurationSec) + fmt.Fprintf(w, "backpressure: count=%d cycles=%d\n", report.BackpressureCount, report.BackpressureCycles) fmt.Fprintf(w, "active tiles: %d\n", report.ActiveTileCount) if report.Passed != nil { fmt.Fprintf(w, "passed: %t\n", *report.Passed) @@ -313,9 +441,15 @@ func PrintSummaryToWriter(report Report, w io.Writer) { idx+1, tile.Coord, tile.UtilizationPct, tile.ActiveCycles, tile.TotalEvents) } } + if len(report.TopBackpressureTiles) > 0 { + fmt.Fprintln(w, "top backpressure tiles:") + for idx, tile := range report.TopBackpressureTiles { + fmt.Fprintf(w, " %d) %s bp=%d\n", idx+1, tile.Coord, tile.BackpressureCount) + } + } } -func classifyAndCount(event traceEvent, acc *tileAccumulator) { +func classifyAndCount(event traceEvent, acc *tileAccumulator, cycle int64, hasCycle bool) bool { switch event.Msg { case "Inst": acc.instCount++ @@ -331,7 +465,14 @@ func classifyAndCount(event traceEvent, acc *tileAccumulator) { acc.recvCount++ } acc.totalEvents++ + case "Backpressure": + acc.backpressureCount++ + if hasCycle { + acc.backpressureCycles[cycle] = struct{}{} + } + return true } + return false } func resolveTileCoord(event traceEvent) (tileCoord, bool) { @@ -379,13 +520,14 @@ func parseTileFromEndpoint(endpoint string) (tileCoord, bool) { return tileCoord{}, false } - var x int - var y int - if _, err := fmt.Sscanf(matches[0], "Device.Tile[%d][%d].Core.", &x, &y); err != nil { + var row int + var col int + if _, err := fmt.Sscanf(matches[0], "Device.Tile[%d][%d].Core.", &row, &col); err != nil { return tileCoord{}, false } - return tileCoord{x: x, y: y}, true + // Endpoint naming is Tile[row][col], while report coordinates are (x=col, y=row). + return tileCoord{x: col, y: row}, true } func parseCycle(timeValue *float64) (int64, bool) { @@ -438,6 +580,39 @@ func buildTopHotTiles(tiles []TileStats, topN int) []TopHotTile { return out } +func buildTopBackpressureTiles(tiles []TileStats, topN int) []TopBackpressureTile { + if len(tiles) == 0 || topN <= 0 { + return nil + } + tmp := make([]TileStats, len(tiles)) + copy(tmp, tiles) + sort.Slice(tmp, func(i, j int) bool { + if tmp[i].BackpressureCount != tmp[j].BackpressureCount { + return tmp[i].BackpressureCount > tmp[j].BackpressureCount + } + if tmp[i].Y != tmp[j].Y { + return tmp[i].Y < tmp[j].Y + } + return tmp[i].X < tmp[j].X + }) + if topN > len(tmp) { + topN = len(tmp) + } + out := make([]TopBackpressureTile, 0, topN) + for i := 0; i < topN; i++ { + if tmp[i].BackpressureCount <= 0 { + continue + } + out = append(out, TopBackpressureTile{ + X: tmp[i].X, + Y: tmp[i].Y, + Coord: tmp[i].Coord, + BackpressureCount: tmp[i].BackpressureCount, + }) + } + return out +} + func formatCoord(x, y int) string { return fmt.Sprintf("(%d,%d)", x, y) } diff --git a/runtimecfg/report.go b/runtimecfg/report.go index e32d46d..a45daa0 100644 --- a/runtimecfg/report.go +++ b/runtimecfg/report.go @@ -10,9 +10,6 @@ const defaultTopN = 5 // BuildReportOptions builds report options from resolved runtime configuration. func (r *Runtime) BuildReportOptions(topN int, passed *bool, mismatchCount *int) (report.GenerateOptions, error) { - if !r.Config.LoggingEnabled { - return report.GenerateOptions{}, fmt.Errorf("logging is disabled, cannot build report options from trace log") - } if r.Config.LogPath == "" { return report.GenerateOptions{}, fmt.Errorf("log path is empty, cannot build report options") } @@ -43,9 +40,14 @@ func (r *Runtime) GenerateAndSaveReport(topN int, passed *bool, mismatchCount *i return report.Report{}, "", err } - result, err := report.GenerateFromLog(opts) - if err != nil { - return report.Report{}, "", fmt.Errorf("generate report from log: %w", err) + var result report.Report + if r.Observer != nil { + result = r.Observer.Build(opts) + } else { + result, err = report.GenerateFromLog(opts) + if err != nil { + return report.Report{}, "", fmt.Errorf("generate report from log: %w", err) + } } reportPath := r.DefaultReportPath() diff --git a/runtimecfg/runtime.go b/runtimecfg/runtime.go index 1e744d6..c246c3c 100644 --- a/runtimecfg/runtime.go +++ b/runtimecfg/runtime.go @@ -1,6 +1,7 @@ package runtimecfg import ( + "context" "fmt" "log/slog" "os" @@ -12,15 +13,18 @@ import ( "github.com/sarchlab/zeonica/api" "github.com/sarchlab/zeonica/cgra" "github.com/sarchlab/zeonica/config" + "github.com/sarchlab/zeonica/core" + "github.com/sarchlab/zeonica/report" ) const ( - defaultRows = 4 - defaultColumns = 4 - defaultExecutionModel = "serial" - defaultDriverName = "Driver" - defaultDeviceName = "Device" - defaultLogTemplate = ".json.log" + defaultRows = 4 + defaultColumns = 4 + defaultExecutionModel = "serial" + defaultExecutionPolicy = "in_order_dataflow" + defaultDriverName = "Driver" + defaultDeviceName = "Device" + defaultLogTemplate = ".json.log" ) var freqPattern = regexp.MustCompile(`^([0-9]+)\s*(ghz|mhz|khz|hz)$`) @@ -31,12 +35,14 @@ type ResolvedConfig struct { Rows int Columns int ExecutionModel string + ExecutionPolicy string DriverName string DriverFreq sim.Freq DeviceName string DeviceFreq sim.Freq BindToArchitecture bool LoggingEnabled bool + EnableTrace bool LogPath string } @@ -54,6 +60,7 @@ type Runtime struct { Engine sim.Engine Driver api.Driver Device cgra.Device + Observer *report.Observer } // LoadRuntime loads arch spec, resolves config, and builds runtime objects. @@ -84,12 +91,20 @@ func Resolve(spec ArchSpec, testName string) (ResolvedConfig, error) { Rows: defaultOrPositive(spec.CGRADefaults.Rows, defaultRows), Columns: defaultOrPositive(spec.CGRADefaults.Columns, defaultColumns), ExecutionModel: defaultOrString(spec.Simulator.ExecutionModel, defaultExecutionModel), + ExecutionPolicy: defaultOrString(spec.Simulator.ExecutionPolicy, defaultExecutionPolicy), DriverName: defaultOrString(spec.Simulator.Driver.Name, defaultDriverName), DeviceName: defaultOrString(spec.Simulator.Device.Name, defaultDeviceName), BindToArchitecture: defaultOrBool(spec.Simulator.Device.BindToArchitecture, true), LoggingEnabled: defaultOrBool(spec.Simulator.Logging.Enabled, true), + EnableTrace: defaultOrBool(spec.Simulator.Logging.EnableTrace, false), } + normalizedPolicy, err := normalizeExecutionPolicy(resolved.ExecutionPolicy) + if err != nil { + return ResolvedConfig{}, err + } + resolved.ExecutionPolicy = normalizedPolicy + driverFreq, err := parseFrequency(spec.Simulator.Driver.Frequency, 1*sim.GHz) if err != nil { return ResolvedConfig{}, fmt.Errorf("resolve driver frequency: %w", err) @@ -140,6 +155,7 @@ func BuildRuntime(cfg ResolvedConfig, overrides *BuildOverrides) (*Runtime, erro WithFreq(cfg.DeviceFreq). WithWidth(width). WithHeight(height). + WithExecutionPolicy(cfg.ExecutionPolicy). Build(cfg.DeviceName) driver.RegisterDevice(device) @@ -149,24 +165,38 @@ func BuildRuntime(cfg ResolvedConfig, overrides *BuildOverrides) (*Runtime, erro Engine: engine, Driver: driver, Device: device, + Observer: report.NewObserver(), }, nil } // InitTraceLogger initializes the default slog JSON trace logger. func (r *Runtime) InitTraceLogger(level slog.Leveler) (*os.File, error) { - if !r.Config.LoggingEnabled { - return nil, nil - } - file, err := os.Create(r.Config.LogPath) if err != nil { return nil, fmt.Errorf("create trace log file: %w", err) } - handler := slog.NewJSONHandler(file, &slog.HandlerOptions{ + core.SetTraceObserver(nil) + if r.Observer != nil { + core.SetTraceObserver(r.Observer.Observe) + } + core.SetTraceEnabled(r.Config.EnableTrace) + + if !r.Config.LoggingEnabled || !r.Config.EnableTrace { + stdoutHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelError, + }) + slog.SetDefault(slog.New(stdoutHandler)) + return file, nil + } + + traceHandler := slog.NewJSONHandler(file, &slog.HandlerOptions{ Level: level, }) - slog.SetDefault(slog.New(handler)) + stdoutHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelError, + }) + slog.SetDefault(slog.New(newTeeHandler(stdoutHandler, traceHandler))) return file, nil } @@ -254,3 +284,71 @@ func parseFrequency(input string, fallback sim.Freq) (sim.Freq, error) { return 0, fmt.Errorf("unsupported frequency unit %q", matches[2]) } } + +func normalizeExecutionPolicy(input string) (string, error) { + text := strings.ToLower(strings.TrimSpace(input)) + switch text { + case "", "in_order_dataflow", "in-order-dataflow", "dynamic": + return "in_order_dataflow", nil + case "elastic_scheduled", "elastic-scheduled", "hybrid": + return "elastic_scheduled", nil + case "strict_timed", "strict-timed", "static": + return "strict_timed", nil + default: + return "", fmt.Errorf( + "unsupported execution_policy %q (supported: strict_timed, elastic_scheduled, in_order_dataflow)", + input, + ) + } +} + +type teeHandler struct { + handlers []slog.Handler +} + +func newTeeHandler(handlers ...slog.Handler) slog.Handler { + cleaned := make([]slog.Handler, 0, len(handlers)) + for _, handler := range handlers { + if handler != nil { + cleaned = append(cleaned, handler) + } + } + return &teeHandler{handlers: cleaned} +} + +func (h *teeHandler) Enabled(ctx context.Context, level slog.Level) bool { + for _, handler := range h.handlers { + if handler.Enabled(ctx, level) { + return true + } + } + return false +} + +func (h *teeHandler) Handle(ctx context.Context, record slog.Record) error { + for _, handler := range h.handlers { + if !handler.Enabled(ctx, record.Level) { + continue + } + if err := handler.Handle(ctx, record.Clone()); err != nil { + return err + } + } + return nil +} + +func (h *teeHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + next := make([]slog.Handler, 0, len(h.handlers)) + for _, handler := range h.handlers { + next = append(next, handler.WithAttrs(attrs)) + } + return &teeHandler{handlers: next} +} + +func (h *teeHandler) WithGroup(name string) slog.Handler { + next := make([]slog.Handler, 0, len(h.handlers)) + for _, handler := range h.handlers { + next = append(next, handler.WithGroup(name)) + } + return &teeHandler{handlers: next} +} diff --git a/runtimecfg/runtime_test.go b/runtimecfg/runtime_test.go new file mode 100644 index 0000000..f74d042 --- /dev/null +++ b/runtimecfg/runtime_test.go @@ -0,0 +1,46 @@ +package runtimecfg + +import ( + "strings" + "testing" +) + +func TestResolveExecutionPolicyDefaultsToInOrder(t *testing.T) { + cfg, err := Resolve(ArchSpec{}, "policy-default") + if err != nil { + t.Fatalf("Resolve returned error: %v", err) + } + if cfg.ExecutionPolicy != "in_order_dataflow" { + t.Fatalf("unexpected default execution policy: %q", cfg.ExecutionPolicy) + } +} + +func TestResolveExecutionPolicyAlias(t *testing.T) { + spec := ArchSpec{ + Simulator: Simulator{ + ExecutionPolicy: "hybrid", + }, + } + cfg, err := Resolve(spec, "policy-alias") + if err != nil { + t.Fatalf("Resolve returned error: %v", err) + } + if cfg.ExecutionPolicy != "elastic_scheduled" { + t.Fatalf("unexpected normalized policy: %q", cfg.ExecutionPolicy) + } +} + +func TestResolveExecutionPolicyInvalid(t *testing.T) { + spec := ArchSpec{ + Simulator: Simulator{ + ExecutionPolicy: "unknown_mode", + }, + } + _, err := Resolve(spec, "policy-invalid") + if err == nil { + t.Fatal("expected error for invalid policy, got nil") + } + if !strings.Contains(err.Error(), "unsupported execution_policy") { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/runtimecfg/spec.go b/runtimecfg/spec.go index 1325203..28dd705 100644 --- a/runtimecfg/spec.go +++ b/runtimecfg/spec.go @@ -26,18 +26,20 @@ type CGRADefaults struct { // Simulator contains simulator runtime settings from arch spec. type Simulator struct { - ExecutionModel string `yaml:"execution_model"` - Logging SimulatorLogging `yaml:"logging"` - Driver NamedComponent `yaml:"driver"` - Device DeviceComponent `yaml:"device"` - Extra map[string]any `yaml:",inline"` + ExecutionModel string `yaml:"execution_model"` + ExecutionPolicy string `yaml:"execution_policy"` + Logging SimulatorLogging `yaml:"logging"` + Driver NamedComponent `yaml:"driver"` + Device DeviceComponent `yaml:"device"` + Extra map[string]any `yaml:",inline"` } // SimulatorLogging configures trace logging behavior. type SimulatorLogging struct { - Enabled *bool `yaml:"enabled"` - File string `yaml:"file"` - Extra map[string]any `yaml:",inline"` + Enabled *bool `yaml:"enabled"` + EnableTrace *bool `yaml:"enableTrace"` + File string `yaml:"file"` + Extra map[string]any `yaml:",inline"` } // NamedComponent contains shared component naming/frequency fields. diff --git a/test/arch_spec/arch_spec.yaml b/test/arch_spec/arch_spec.yaml index acb94d3..106f836 100644 --- a/test/arch_spec/arch_spec.yaml +++ b/test/arch_spec/arch_spec.yaml @@ -45,6 +45,8 @@ extensions: simulator: execution_model: "serial" + execution_policy: "in_order_dataflow" + # three policy: strict_timed, elastic_scheduled, in_order_dataflow logging: enabled: true diff --git a/test/testbench/axpy/main.go b/test/testbench/axpy/main.go index 11c1f53..07d4bbf 100644 --- a/test/testbench/axpy/main.go +++ b/test/testbench/axpy/main.go @@ -177,13 +177,9 @@ func main() { } passed := mismatch == 0 - if rt.Config.LoggingEnabled { - reportPath, err := rt.GenerateSaveAndPrintReport(5, &passed, &mismatch) - if err != nil { - panic(err) - } - fmt.Printf("report saved: %s\n", reportPath) - } else { - fmt.Println("logging disabled in arch spec, skipped report generation") + reportPath, err := rt.GenerateSaveAndPrintReport(5, &passed, &mismatch) + if err != nil { + panic(err) } + fmt.Printf("report saved: %s\n", reportPath) } diff --git a/test/testbench/branch_for/main.go b/test/testbench/branch_for/main.go index dfc7055..02b80bc 100644 --- a/test/testbench/branch_for/main.go +++ b/test/testbench/branch_for/main.go @@ -146,13 +146,9 @@ func main() { } passed := mismatch == 0 - if rt.Config.LoggingEnabled { - reportPath, err := rt.GenerateSaveAndPrintReport(5, &passed, &mismatch) - if err != nil { - panic(err) - } - fmt.Printf("report saved: %s\n", reportPath) - } else { - fmt.Println("logging disabled in arch spec, skipped report generation") + reportPath, err := rt.GenerateSaveAndPrintReport(5, &passed, &mismatch) + if err != nil { + panic(err) } + fmt.Printf("report saved: %s\n", reportPath) } diff --git a/test/testbench/fir/main.go b/test/testbench/fir/main.go index 93197bd..b5a657b 100644 --- a/test/testbench/fir/main.go +++ b/test/testbench/fir/main.go @@ -166,13 +166,9 @@ func main() { } passed := mismatchCount == 0 - if rt.Config.LoggingEnabled { - reportPath, err := rt.GenerateSaveAndPrintReport(5, &passed, &mismatchCount) - if err != nil { - panic(err) - } - fmt.Printf("report saved: %s\n", reportPath) - } else { - fmt.Println("logging disabled in arch spec, skipped report generation") + reportPath, err := rt.GenerateSaveAndPrintReport(5, &passed, &mismatchCount) + if err != nil { + panic(err) } + fmt.Printf("report saved: %s\n", reportPath) } diff --git a/test/testbench/histogram/main.go b/test/testbench/histogram/main.go index 3376de8..1b58a51 100644 --- a/test/testbench/histogram/main.go +++ b/test/testbench/histogram/main.go @@ -198,13 +198,9 @@ func main() { } passed := mismatch == 0 - if rt.Config.LoggingEnabled { - reportPath, err := rt.GenerateSaveAndPrintReport(5, &passed, &mismatch) - if err != nil { - panic(err) - } - fmt.Printf("report saved: %s\n", reportPath) - } else { - fmt.Println("logging disabled in arch spec, skipped report generation") + reportPath, err := rt.GenerateSaveAndPrintReport(5, &passed, &mismatch) + if err != nil { + panic(err) } + fmt.Printf("report saved: %s\n", reportPath) } diff --git a/test/testbench/policy_behavior/late_arrival.yaml b/test/testbench/policy_behavior/late_arrival.yaml new file mode 100644 index 0000000..4092381 --- /dev/null +++ b/test/testbench/policy_behavior/late_arrival.yaml @@ -0,0 +1,47 @@ +array_config: + columns: 2 + rows: 1 + compiled_ii: 8 + cores: + - column: 0 + row: 0 + core_id: "relay" + entries: + - entry_id: "entry0" + instructions: + - index_per_ii: 0 + operations: + - opcode: "DATA_MOV" + id: 0 + time_step: 0 + invalid_iterations: 0 + src_operands: + - operand: "WEST" + color: "RED" + dst_operands: + - operand: "EAST" + color: "RED" + - column: 1 + row: 0 + core_id: "sink" + entries: + - entry_id: "entry0" + instructions: + - index_per_ii: 0 + operations: + - opcode: "STORE" + id: 1 + time_step: 0 + invalid_iterations: 0 + src_operands: + - operand: "WEST" + color: "RED" + - operand: "0" + color: "RED" + - opcode: "RETURN_VALUE" + id: 2 + time_step: 0 + invalid_iterations: 0 + src_operands: + - operand: "1" + color: "RED" diff --git a/test/testbench/policy_behavior/policy_behavior_test.go b/test/testbench/policy_behavior/policy_behavior_test.go new file mode 100644 index 0000000..208f7df --- /dev/null +++ b/test/testbench/policy_behavior/policy_behavior_test.go @@ -0,0 +1,137 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/sarchlab/akita/v4/sim" + "github.com/sarchlab/zeonica/cgra" + "github.com/sarchlab/zeonica/core" + "github.com/sarchlab/zeonica/runtimecfg" +) + +type runResult struct { + panicMsg string + memValue uint32 + retValue uint32 + endNS int64 +} + +func resolveScenarioPath(t *testing.T, filename string) string { + t.Helper() + + _, thisFile, _, ok := runtime.Caller(0) + if !ok { + t.Fatalf("cannot resolve current test file path") + } + + path := filepath.Clean(filepath.Join(filepath.Dir(thisFile), filename)) + if _, err := os.Stat(path); err != nil { + t.Fatalf("scenario file %s not found: %v", path, err) + } + return path +} + +func writePolicyArchSpec(t *testing.T, policy string) string { + t.Helper() + + spec := fmt.Sprintf(`cgra_defaults: + rows: 1 + columns: 2 +simulator: + execution_model: "serial" + execution_policy: "%s" + logging: + enabled: false + driver: + name: "Driver" + frequency: "1GHz" + device: + name: "Device" + frequency: "1GHz" + bind_to_architecture: true +`, policy) + + specPath := filepath.Join(t.TempDir(), "arch_spec.yaml") + if err := os.WriteFile(specPath, []byte(spec), 0o600); err != nil { + t.Fatalf("write arch spec: %v", err) + } + return specPath +} + +func runWorkloadWithPolicy(t *testing.T, policy, scenarioPath string) (result runResult) { + t.Helper() + + defer func() { + if recovered := recover(); recovered != nil { + result.panicMsg = fmt.Sprint(recovered) + } + }() + + specPath := writePolicyArchSpec(t, policy) + rt, err := runtimecfg.LoadRuntime(specPath, "policy_behavior_"+policy) + if err != nil { + t.Fatalf("load runtime: %v", err) + } + + program := core.LoadProgramFileFromYAML(scenarioPath) + if len(program) == 0 { + t.Fatalf("empty program map from %s", scenarioPath) + } + + width := rt.Config.Columns + height := rt.Config.Rows + for x := 0; x < width; x++ { + for y := 0; y < height; y++ { + coord := fmt.Sprintf("(%d,%d)", x, y) + if prog, exists := program[coord]; exists { + rt.Driver.MapProgram(prog, [2]int{x, y}) + } + } + } + + for x := 0; x < width; x++ { + for y := 0; y < height; y++ { + tile := rt.Device.GetTile(x, y) + rt.Engine.Schedule(sim.MakeTickEvent(tile.GetTickingComponent(), 0)) + } + } + + rt.Driver.FeedIn([]uint32{42}, cgra.West, [2]int{0, 1}, 1, "R") + rt.Driver.Run() + + result.memValue = rt.Driver.ReadMemory(1, 0, 0) + result.retValue = rt.Device.GetTile(1, 0).GetRetVal() + result.endNS = int64(rt.Engine.CurrentTime() * 1e9) + + return result +} + +func TestPolicyBehaviorLateArrival(t *testing.T) { + scenarioPath := resolveScenarioPath(t, "late_arrival.yaml") + + strict := runWorkloadWithPolicy(t, "strict_timed", scenarioPath) + if !strings.Contains(strict.panicMsg, "synchronization violation") { + t.Fatalf("strict_timed should report synchronization violation, got: %q", strict.panicMsg) + } + + elastic := runWorkloadWithPolicy(t, "elastic_scheduled", scenarioPath) + if elastic.panicMsg != "" { + t.Fatalf("elastic_scheduled should tolerate late arrival, got panic: %s", elastic.panicMsg) + } + if elastic.memValue != 42 || elastic.retValue != 1 { + t.Fatalf("elastic_scheduled wrong result: mem=%d ret=%d want mem=42 ret=1", elastic.memValue, elastic.retValue) + } + + inOrder := runWorkloadWithPolicy(t, "in_order_dataflow", scenarioPath) + if inOrder.panicMsg != "" { + t.Fatalf("in_order_dataflow should tolerate late arrival, got panic: %s", inOrder.panicMsg) + } + if inOrder.memValue != 42 || inOrder.retValue != 1 { + t.Fatalf("in_order_dataflow wrong result: mem=%d ret=%d want mem=42 ret=1", inOrder.memValue, inOrder.retValue) + } +} diff --git a/test/testbench/relu/main.go b/test/testbench/relu/main.go index e47dd49..1ff2085 100644 --- a/test/testbench/relu/main.go +++ b/test/testbench/relu/main.go @@ -2,47 +2,36 @@ package main import ( "fmt" - "log/slog" "os" + "path/filepath" + "runtime" + "strings" "github.com/sarchlab/akita/v4/sim" - "github.com/sarchlab/zeonica/api" - "github.com/sarchlab/zeonica/config" "github.com/sarchlab/zeonica/core" + "github.com/sarchlab/zeonica/runtimecfg" ) -func Relu() { - width := 4 - height := 4 - - engine := sim.NewSerialEngine() - - driver := api.DriverBuilder{}. - WithEngine(engine). - WithFreq(1 * sim.GHz). - Build("Driver") - - device := config.DeviceBuilder{}. - WithEngine(engine). - WithFreq(1 * sim.GHz). - WithWidth(width). - WithHeight(height). - Build("Device") - - driver.RegisterDevice(device) - - programPath := "test/testbench/relu/relu.yaml" - - // preload data - - data := []int32{1, -2, 3, -4, 5, -6, 7, -8, 9, -10, 11, -12, 13, 14, -15, 16, 17, 18, 19, 20, -21, 22, 23, 24, -25, 26, 27, 28, -29, 30, -31, 32} // length is 32 - - for i := 0; i < len(data); i++ { - driver.PreloadMemory(3, 2, uint32(data[i]), uint32(i)) +// Relu runs the ReLU testbench on the configured runtime. +// +//nolint:gocyclo +func Relu(rt *runtimecfg.Runtime) int { + width := rt.Config.Columns + height := rt.Config.Rows + + driver := rt.Driver + device := rt.Device + engine := rt.Engine + + programPath := strings.TrimSpace(os.Getenv("ZEONICA_PROGRAM_YAML")) + if programPath == "" { + if _, err := os.Stat("relu.yaml"); err == nil { + programPath = "relu.yaml" + } else { + programPath = "relu/relu.yaml" + } } - program := core.LoadProgramFileFromYAML(programPath) - fmt.Println("program:", program) if len(program) == 0 { @@ -58,43 +47,132 @@ func Relu() { } } + // preload input data at tile (3,2): 32 int32 values + inputData := []int32{1, -2, 3, -4, 5, -6, 7, -8, 9, -10, 11, -12, 13, 14, -15, 16, 17, 18, 19, 20, -21, 22, 23, 24, -25, 26, 27, 28, -29, 30, -31, 32} + for i := 0; i < len(inputData); i++ { + driver.PreloadMemory(3, 2, uint32(inputData[i]), uint32(i)) + } + // fire all the cores in the beginning for x := 0; x < width; x++ { for y := 0; y < height; y++ { tile := device.GetTile(x, y) - // convert to tileCore tickingComponent := tile.GetTickingComponent() engine.Schedule(sim.MakeTickEvent(tickingComponent, 0)) } } - // TODO: Add PreloadMemory calls if needed for relu test - // driver.PreloadMemory(x, y, data, baseAddr) - driver.Run() fmt.Println("========================") fmt.Println("========================") fmt.Println("========================") - // get memory values in (1,3) from 0x0-0x31 - for i := 0; i < 32; i++ { - value := driver.ReadMemory(1, 3, uint32(i)) - fmt.Println("memory[", i, "]:", value) + // output tile (1,3), 32 elements + outputTile := [2]int{1, 3} + scanLimit := 32 + fmt.Printf("output memory @ tile (%d,%d):\n", outputTile[0], outputTile[1]) + outputData := make([]uint32, scanLimit) + for addr := 0; addr < scanLimit; addr++ { + val := driver.ReadMemory(outputTile[0], outputTile[1], uint32(addr)) + outputData[addr] = val + fmt.Printf(" addr %d -> %d\n", addr, val) + } + + expected := computeReLU(inputData) + fmt.Println("expected ReLU (CPU):") + reluMismatch := 0 + for i, val := range expected { + fmt.Printf(" addr %d -> %d\n", i, val) + if i < len(outputData) && outputData[i] != val { + reluMismatch++ + } + } + if reluMismatch == 0 { + fmt.Println("✅ output matches expected ReLU") + } else { + fmt.Printf("❌ output mismatches ReLU: %d\n", reluMismatch) + } + return reluMismatch +} + +func computeReLU(input []int32) []uint32 { + out := make([]uint32, len(input)) + for i, v := range input { + if v > 0 { + out[i] = uint32(v) + } else { + out[i] = 0 + } + } + return out +} + +func resolveArchSpecPath() (string, error) { + fromEnv := strings.TrimSpace(os.Getenv("ZEONICA_ARCH_SPEC")) + if fromEnv != "" { + if _, err := os.Stat(fromEnv); err == nil { + return fromEnv, nil + } + return "", fmt.Errorf("ZEONICA_ARCH_SPEC points to a missing file: %s", fromEnv) + } + + candidates := []string{ + "test/arch_spec/arch_spec.yaml", + "../../arch_spec/arch_spec.yaml", } + + if _, thisFile, _, ok := runtime.Caller(0); ok { + candidates = append(candidates, + filepath.Clean(filepath.Join(filepath.Dir(thisFile), "..", "..", "arch_spec", "arch_spec.yaml")), + ) + } + + seen := make(map[string]struct{}, len(candidates)) + normalized := make([]string, 0, len(candidates)) + for _, candidate := range candidates { + clean := filepath.Clean(candidate) + if _, exists := seen[clean]; exists { + continue + } + seen[clean] = struct{}{} + normalized = append(normalized, clean) + if _, err := os.Stat(clean); err == nil { + return clean, nil + } + } + + return "", fmt.Errorf("cannot locate arch spec, tried: %s", strings.Join(normalized, ", ")) } func main() { - f, err := os.Create("relu.json.log") + const testName = "relu" + + archSpecPath, err := resolveArchSpecPath() if err != nil { panic(err) } - defer f.Close() - handler := slog.NewJSONHandler(f, &slog.HandlerOptions{ - Level: core.LevelTrace, - }) + rt, err := runtimecfg.LoadRuntime(archSpecPath, testName) + if err != nil { + panic(err) + } + + traceLog, err := rt.InitTraceLogger(core.LevelTrace) + if err != nil { + panic(err) + } + + mismatch := Relu(rt) - slog.SetDefault(slog.New(handler)) - Relu() + if err := runtimecfg.CloseTraceLog(traceLog); err != nil { + panic(err) + } + + passed := mismatch == 0 + reportPath, err := rt.GenerateSaveAndPrintReport(5, &passed, &mismatch) + if err != nil { + panic(err) + } + fmt.Printf("report saved: %s\n", reportPath) } diff --git a/tool/viz/README.md b/tool/viz/README.md index 6f6c54c..7af721d 100644 --- a/tool/viz/README.md +++ b/tool/viz/README.md @@ -1,6 +1,9 @@ # CGRA Log Viewer -This viewer visualizes JSONL traces like `gemm.json.log` with a cycle slider and playback. +This viewer has two synchronized views: + +- Timeline replay for JSONL traces (cycle slider + playback) +- **Strict Timing Offset View** (program YAML + trace correlation by op ID) ## Run @@ -13,13 +16,155 @@ python3 -m http.server 8000 Open: ```text -http://localhost:8000/viz/ +http://localhost:8000/tool/viz/ ``` -It will try to load `../gemm.json.log` automatically. You can also load any other trace from the file picker. +The page tries to auto-load files (example): + +- `../gemm.json.log` (trace) +- `../gemm.yaml` (program) + +If not found, use file pickers manually. + +## Inputs + +You need both files to get strict timing comparison: + +- **Trace log**: JSONL with `Inst` events (`X`, `Y`, `ID`, `Time`) +- **Program YAML**: includes `array_config.compiled_ii`, per-core operations with `id` and `time_step` + +Optional aggregate report input: + +- **Report JSON**: generated report (for example `fir.report.json`) with `grid`, global counters, per-tile `utilizationPct`, and `topHotTiles`. +- Report can be loaded independently from trace/yaml for quick utilization review. +- Backpressure metrics are supported when report includes runtime `Backpressure` events: + - `backpressureCount`: total downstream backpressure hits (`SendBufBusy`) + - `backpressureCycles`: cycles containing at least one backpressure hit + - `tiles[].backpressureCount` and `topBackpressureTiles` + +## Strict Timing Offset View + +Layout behavior: + +- Top grid uses hybrid adaptation based on detected grid size: + - If YAML provides `array_config.columns/rows`, mesh size uses YAML array bounds first. + - If YAML is unavailable, bounds are inferred from trace events. + - Prefer fitting into current canvas viewport by scaling tile/gap. + - If tile would become too small, switch to expanded `viewBox` to keep readability. +- Top mesh supports free zoom/pan (wheel zoom + drag pan) for large arrays. +- If report is loaded, mesh adds a utilization heat overlay from `tiles[].utilizationPct` (missing tiles treated as 0). +- Active tiles render per-cycle summary text in-tile: + - `OP:` instruction opcode summary + - `MEM:` direct memory behaviors (e.g. `LoadDirect` / `StoreDirect`) + - `RX:` / `TX:` data snippets from `Send` / `Recv` / `FeedIn` / `Collect` +- DataFlow links keep pulse animation and include inline data labels from trace `Data` fields (deduplicated for same path/data in one cycle). +- Bottom timing view uses a timeline axis (`Y=core`, `X=cycle`) + drilldown. + +Report view: + +- Report panel shows summary cards: `totalCycles`, `activeCyclesGlobal`, `idleCyclesGlobal`, `passed`, `mismatchCount`, `activeTileCount`, `totalEvents`. +- Hot-tile table shows ranked `coord`, `utilizationPct`, `activeCycles`, `totalEvents`. +- Backpressure section shows ranked `coord`, `bp-count` from `topBackpressureTiles` (if available). +- If report grid and current mesh grid differ, viewer shows a warning; overlay is clipped to current mesh bounds. + +Timing view layout: + +- One lane per core `(x,y)` with: + - upper sub-row blocks: Expected slots + - lower sub-row blocks: Actual slots (all samples in full trace) +- Timeline blocks are expanded by **all actual samples across full trace length** (not just first occurrence). +- For each actual sample occurrence, expected block is back-computed and aligned by that sample's delta. +- Fractional `Time` values are rounded with `Math.round` before slot/time comparison and rendering. +- `baseline-view` supports: + - `strict`: strict baseline only + - `compensated`: compensated baseline only + - `split`: strict + compensated side-by-side rows for comparison +- Mismatch blocks/links are drawn as rectangles (not points) for slot-level readability +- Drilldown panel still shows operation-level details for selected `(core, slot)` +- `window-start` + `window-size` let you pan/zoom through full trace cycles +- Optional IO waveform expansion supports multiple cores: + - `io-wave-all`: expand waveform rows for all visible cores + - `io-wave-core`: multi-select a subset of cores to expand + - double-click Y-axis core label: toggle that core's IO wave quickly +- Expanded IO rows are bus-style waveform segments (trapezoid/diamond transition with parallel top and bottom edges): + - `IN` row: DataFlow values from `FeedIn(to tile)` and `Recv(dst tile)` + - `OUT` row: DataFlow values from `Send(src tile)` and `Collect(from tile)` +- IO waveform values are rendered in signed decimal; when multiple values occur in one cycle, the waveform label shows a compact summary and full values remain in tooltip. + +Default view (hybrid as main): + +- **Default** is `baseline-view=compensated` and `comp-model=hybrid`. The timeline and anomaly filter use **hybrid** status only, so you focus on "mid-trace" offsets after subtracting expected propagation delay; strict remains in summary and drilldown for reference. +- Use `strict` or `split` only when you want to double-check raw schedule vs trace or debug compiler/schedule issues. + +Default interaction: + +- `anomaly-only` is disabled by default; when in compensated view it filters by **hybrid** status (not strict). +- `show-phase-explain` is enabled by default to expose per-core phase offsets +- `boundary-only` can focus edge PEs to verify boundary shift patterns quickly +- **Jump to first hybrid mismatch** button moves the time window to the first cycle where any op is a hybrid mismatch. +- `Ctrl + mouse wheel` zooms timeline quickly (X/Y together). Zoom anchor follows mouse position on X-axis to reduce view jump. +- `y-zoom` slider adjusts lane height/readability; `Reset Zoom` restores default zoom and window. +- `comp-model` supports: + - `distance-heuristic`: infer propagation delay from core-to-ingress distance + - `trace-fitted-phase`: use per-core fitted phase (`modeDelta`) + - `hybrid`: prefer fitted when confidence is high, otherwise fall back to distance (default) +- Click a timeline block/link to inspect operation-level details in drilldown +- Drilldown now includes sample source fields so each match can be traced back to `Inst` / `LoadDirect` / `StoreDirect`. +- Core focus supports two synced entry points: click Y-axis core label, or select from `core-focus` dropdown. +- When a core is focused, the main timeline keeps only that core and an inline mini panel shows source distribution plus a compact in-window trace list. +- Y-axis label interaction is split: + - single-click: focus/unfocus core for main timeline + - double-click: toggle IO waveform expansion for that core +- `Export PNG` downloads the current timeline window +- `max-side` controls export scaling upper bound; oversized windows are proportionally downscaled +- For repeated op executions, timeline labels/tooltips include occurrence tag (e.g. `@2` or `[2/5]`). + +Status semantics: + +- **Strict baseline (truth reference, unchanged):** + - `on-time`: `actualSlot == expectedSlot` + - `early`: `actualSlot < expectedSlot` (signed modular delta) + - `late`: `actualSlot > expectedSlot` + - `missing`: operation exists in YAML but no `Inst` with same `(x,y,id)` in trace +- **Compensated baseline (explanation layer):** + - strict delta is rebased by per-core compensation offset + - used to reduce global boundary propagation shift false-positives + - never replaces strict verdict; always shown as secondary comparison + +Phase explanation layer (additive, does not change strict status): + +- `Δcore`: dominant per-core phase offset inferred from mismatch mode (`modeDelta`) +- `conf`: confidence of that offset from mismatch concentration +- `phase(boundary, inner, gap)`: weighted-median phase summary comparing boundary vs inner cores +- `deltaRebased`: per-op delta after subtracting `Δcore` (for separating global shift from local residual anomalies) + +Shift-aware annotations: + +- `first-divergence`: first mismatch point or delta-change point in a core +- `propagated`: same-delta continuation after divergence (faded style) + +Drilldown fields: + +- `opId`, `opcode` +- `expectedSlot`, `actualSlot` +- `deltaStrict` +- `deltaComp()` +- `statusStrict` / `statusComp` +- `deltaPhaseRebased` +- `firstTime` +- `samples` +- `sourceSummary` (for example `Inst*10,LoadDirect*2`) +- `firstDivergence` +- `samplePreview` with source tags (for example `1:210:Inst,2:213:LoadDirect`) + +Recommended read path: + +1. Use default **compensated + hybrid** view to see whether there are any mid-trace offsets (hybrid mismatch). Use "Jump to first hybrid mismatch" to focus the window on the first such cycle. +2. In drilldown, read `statusComp` / `deltaComp(hybrid)` first; treat `statusStrict` / `deltaStrict` as reference only for double-check. +3. If you need to verify raw schedule vs trace, switch to `strict` or `split` and compare; strict is the truth reference for pass/fail. -## Supported events +## Supported event families (timeline view) - `DataFlow` (`FeedIn`, `Send`, `Recv`, `Collect`) -- `Inst` (`DATA_MOV`, `MUL_ADD`, `STORE`) -- `Memory` (`StoreDirect`) +- `Inst` (generic instruction events) +- `Memory` (e.g., `StoreDirect`) diff --git a/tool/viz/__pycache__/run_viz.cpython-313.pyc b/tool/viz/__pycache__/run_viz.cpython-313.pyc new file mode 100644 index 0000000..f91c781 Binary files /dev/null and b/tool/viz/__pycache__/run_viz.cpython-313.pyc differ diff --git a/tool/viz/app.js b/tool/viz/app.js index 13d7c56..2471b50 100644 --- a/tool/viz/app.js +++ b/tool/viz/app.js @@ -1,6 +1,7 @@ const state = { events: [], byTime: new Map(), + timeKeys: [], minTime: 0, maxTime: 0, currentTime: 0, @@ -12,9 +13,52 @@ const state = { showInst: true, showMemory: true, showLabels: true, + programSpec: null, + yamlGridBounds: null, + reportSpec: null, + reportReady: false, + reportError: "", + reportHeatMetric: "utilizationPct", + timingRows: [], + timingColumns: [], + timingReady: false, + layoutMode: "fit", + timingAnomalyOnly: false, + timingSelectedCell: null, + timingFocusedCoreKey: null, + showPhaseExplain: true, + timingBoundaryOnly: false, + timingBaselineView: "compensated", + timingCompModel: "hybrid", + timingIoWaveExpandAll: false, + timingIoWaveExpandedCoreKeys: new Set(), + timingWindowStart: 0, + timingWindowSize: 120, + timingZoomX: 1, + timingZoomY: 1, + timingViewport: null, + firstHybridMismatchTime: null, + coreIoWaveByTime: new Map(), + stepLock: false, }; const layout = { + baseWidth: 940, + baseHeight: 620, + baseTileSize: 100, + baseGap: 24, + baseDriverOffset: 52, + marginLeft: 170, + marginRight: 92, + marginTop: 90, + marginBottom: 88, + minTileSize: 28, + maxTileSize: 124, + minReadableTile: 36, + minGap: 7, + maxGap: 28, + minDriverOffset: 20, + maxDriverOffset: 66, width: 940, height: 620, originX: 170, @@ -34,8 +78,11 @@ const colors = { }; const svg = d3.select("#canvas"); +let sceneRoot; let staticLayer; let dynamicLayer; +let meshZoomBehavior = null; +let meshZoomTransform = d3.zoomIdentity; const controls = { playBtn: document.getElementById("playBtn"), @@ -49,14 +96,142 @@ const controls = { showMemory: document.getElementById("showMemory"), showLabels: document.getElementById("showLabels"), fileInput: document.getElementById("fileInput"), + yamlInput: document.getElementById("yamlInput"), + reportInput: document.getElementById("reportInput"), statsLine: document.getElementById("statsLine"), eventDump: document.getElementById("eventDump"), + reportSummary: document.getElementById("reportSummary"), + reportHotTiles: document.getElementById("reportHotTiles"), + reportWarning: document.getElementById("reportWarning"), + timingSummary: document.getElementById("timingSummary"), + timingGrid: document.getElementById("timingGrid"), + timingAnomalyOnly: document.getElementById("timingAnomalyOnly"), + timingShowPhaseExplain: document.getElementById("timingShowPhaseExplain"), + timingBoundaryOnly: document.getElementById("timingBoundaryOnly"), + timingCoreFocus: document.getElementById("timingCoreFocus"), + timingIoWaveAll: document.getElementById("timingIoWaveAll"), + timingIoWaveCore: document.getElementById("timingIoWaveCore"), + timingBaselineView: document.getElementById("timingBaselineView"), + timingCompModel: document.getElementById("timingCompModel"), + timingWindowStart: document.getElementById("timingWindowStart"), + timingWindowSize: document.getElementById("timingWindowSize"), + timingWindowStartLabel: document.getElementById("timingWindowStartLabel"), + timingWindowSizeLabel: document.getElementById("timingWindowSizeLabel"), + timingZoomY: document.getElementById("timingZoomY"), + timingZoomYLabel: document.getElementById("timingZoomYLabel"), + timingResetZoom: document.getElementById("timingResetZoom"), + timingExportPng: document.getElementById("timingExportPng"), + timingExportMaxSide: document.getElementById("timingExportMaxSide"), + timingJumpFirstMismatch: document.getElementById("timingJumpFirstMismatch"), + timingDrilldown: document.getElementById("timingDrilldown"), + timingCoreMini: document.getElementById("timingCoreMini"), + meshLegend: document.getElementById("meshLegend"), + vizPanel: document.querySelector(".panel.viz"), }; +let timingCoreLabelClickTimer = null; function tileKey(x, y) { return `${x},${y}`; } +function clamp(value, min, max) { + return Math.max(min, Math.min(max, value)); +} + +function normalizeCycleTime(value, fallback = 0) { + const numeric = Math.round(Number(value)); + return Number.isFinite(numeric) ? numeric : fallback; +} + +function nextIndexedTime(current, direction) { + const dir = direction >= 0 ? 1 : -1; + const keys = Array.isArray(state.timeKeys) ? state.timeKeys : []; + const cur = normalizeCycleTime(current, state.minTime); + if (keys.length === 0) { + const target = cur + dir; + return clamp(target, state.minTime, state.maxTime); + } + const exactIdx = keys.indexOf(cur); + if (exactIdx >= 0) { + const nextIdx = clamp(exactIdx + dir, 0, keys.length - 1); + return keys[nextIdx]; + } + if (dir > 0) { + for (const t of keys) { + if (t > cur) return t; + } + return keys[keys.length - 1]; + } + for (let i = keys.length - 1; i >= 0; i -= 1) { + if (keys[i] < cur) return keys[i]; + } + return keys[0]; +} + +function resolveTargetViewport() { + const hostWidth = controls.vizPanel?.clientWidth || layout.baseWidth; + const width = Math.max(720, Math.round(hostWidth) - 8); + const height = Math.max(480, Math.round(width * (layout.baseHeight / layout.baseWidth))); + return { width, height }; +} + +function applyAdaptiveLayout() { + const cols = Math.max(1, state.maxX + 1); + const rows = Math.max(1, state.maxY + 1); + const { width: targetWidth, height: targetHeight } = resolveTargetViewport(); + + const contentW = Math.max(1, targetWidth - layout.marginLeft - layout.marginRight); + const contentH = Math.max(1, targetHeight - layout.marginTop - layout.marginBottom); + const baseGridW = cols * layout.baseTileSize + (cols - 1) * layout.baseGap; + const baseGridH = rows * layout.baseTileSize + (rows - 1) * layout.baseGap; + const fitScale = Math.min(contentW / baseGridW, contentH / baseGridH); + const boundedScale = clamp(fitScale, 0.2, 1.45); + + let tileSize = clamp( + Math.round(layout.baseTileSize * boundedScale), + layout.minTileSize, + layout.maxTileSize, + ); + let gap = clamp(Math.round(layout.baseGap * boundedScale), layout.minGap, layout.maxGap); + let driverOffset = clamp( + Math.round(layout.baseDriverOffset * boundedScale), + layout.minDriverOffset, + layout.maxDriverOffset, + ); + let mode = "fit"; + if (tileSize < layout.minReadableTile) { + mode = "expand"; + tileSize = layout.minReadableTile; + const readableScale = tileSize / layout.baseTileSize; + gap = clamp(Math.round(layout.baseGap * readableScale), layout.minGap, layout.maxGap); + driverOffset = clamp( + Math.round(layout.baseDriverOffset * readableScale), + layout.minDriverOffset, + layout.maxDriverOffset, + ); + } + + const gridW = cols * tileSize + (cols - 1) * gap; + const gridH = rows * tileSize + (rows - 1) * gap; + const neededW = layout.marginLeft + gridW + layout.marginRight; + const neededH = layout.marginTop + gridH + layout.marginBottom; + const width = mode === "expand" ? Math.max(targetWidth, neededW) : targetWidth; + const height = mode === "expand" ? Math.max(targetHeight, neededH) : targetHeight; + + const freeW = width - layout.marginLeft - layout.marginRight - gridW; + const freeH = height - layout.marginTop - layout.marginBottom - gridH; + layout.width = width; + layout.height = height; + layout.tileSize = tileSize; + layout.gap = gap; + layout.driverOffset = driverOffset; + layout.originX = layout.marginLeft + Math.max(0, Math.floor(freeW / 2)); + layout.originY = layout.marginTop + Math.max(0, Math.floor(freeH / 2)); + state.layoutMode = mode; + + svg.attr("viewBox", `0 0 ${layout.width} ${layout.height}`); +} + function tileRect(x, y) { const step = layout.tileSize + layout.gap; const px = layout.originX + x * step; @@ -142,14 +317,56 @@ function inferBounds(events) { return { maxX, maxY }; } +function boundsFromProgramSpec(programSpec) { + if (!programSpec) return null; + const cols = Number(programSpec.arrayColumns); + const rows = Number(programSpec.arrayRows); + if (!Number.isFinite(cols) || !Number.isFinite(rows) || cols <= 0 || rows <= 0) return null; + return { + maxX: Math.max(0, Math.round(cols) - 1), + maxY: Math.max(0, Math.round(rows) - 1), + }; +} + +function boundsFromReportSpec(reportSpec) { + if (!reportSpec?.grid) return null; + const width = Number(reportSpec.grid.width); + const height = Number(reportSpec.grid.height); + if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return null; + return { + maxX: Math.max(0, Math.round(width) - 1), + maxY: Math.max(0, Math.round(height) - 1), + }; +} + +function resolveMeshBounds(events) { + const yamlBounds = boundsFromProgramSpec(state.programSpec); + if (yamlBounds) return yamlBounds; + const traceBounds = inferBounds(events); + const hasTraceBounds = traceBounds.maxX > 0 || traceBounds.maxY > 0; + if (hasTraceBounds) return traceBounds; + const reportBounds = boundsFromReportSpec(state.reportSpec); + if (reportBounds) return reportBounds; + return traceBounds; +} + function parseJsonLines(text) { const lines = text.split(/\r?\n/).map((s) => s.trim()).filter(Boolean); const rows = []; + let lastTime = null; for (const line of lines) { try { const obj = JSON.parse(line); - if (obj && typeof obj.Time === "number" && Number.isFinite(obj.Time)) { - obj.Time = Math.round(obj.Time); + if (obj && Number.isFinite(Number(obj.Time))) { + obj.Time = Math.round(Number(obj.Time)); + lastTime = obj.Time; + rows.push(obj); + continue; + } + // Some memory traces (e.g. LoadDirect/StoreDirect) may omit Time. + // Reuse the latest observed cycle to keep them alignable in strict matching. + if (obj && obj.msg === "Memory" && Number.isFinite(lastTime)) { + obj.Time = lastTime; rows.push(obj); } } catch (_) { @@ -174,7 +391,1962 @@ function indexByTime(events) { minTime = 0; maxTime = 0; } - return { byTime, minTime, maxTime }; + const sortedTimes = [...byTime.keys()].sort((a, b) => a - b); + return { byTime, minTime, maxTime, sortedTimes }; +} + +function normalizeSlot(value, ii) { + const v = Math.round(Number(value)); + if (!Number.isFinite(v)) return 0; + if (ii > 0) { + let slot = v % ii; + if (slot < 0) slot += ii; + return slot; + } + return v; +} + +function signedDelta(actualSlot, expectedSlot, ii) { + if (!Number.isFinite(actualSlot) || !Number.isFinite(expectedSlot)) return null; + if (ii <= 0) return actualSlot - expectedSlot; + const raw = normalizeSlot(actualSlot - expectedSlot, ii); + if (raw === 0) return 0; + return raw <= ii / 2 ? raw : raw - ii; +} + +function sortCore(a, b) { + if (b.y !== a.y) return b.y - a.y; + return a.x - b.x; +} + +function escapeHtml(text) { + return String(text) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """); +} + +function formatDelta(delta) { + if (delta == null || !Number.isFinite(Number(delta))) return "N/A"; + const v = Number(delta); + return `${v >= 0 ? "+" : ""}${v}`; +} + +function formatDataAsDecimal(value) { + if (value == null) return null; + const numeric = Number(value); + if (Number.isFinite(numeric)) { + if (Number.isInteger(numeric)) return String(numeric); + return String(numeric); + } + const text = String(value).trim(); + return text.length > 0 ? text : null; +} + +function numberOr(value, fallback = 0) { + const v = Number(value); + return Number.isFinite(v) ? v : fallback; +} + +function integerOr(value, fallback = 0) { + return Math.round(numberOr(value, fallback)); +} + +function nullableBool(value) { + if (typeof value === "boolean") return value; + return null; +} + +function parseReportJson(text) { + let raw; + try { + raw = JSON.parse(text); + } catch (err) { + throw new Error(`Invalid JSON: ${err.message}`); + } + if (!raw || typeof raw !== "object") { + throw new Error("Report root must be an object."); + } + + const tiles = (Array.isArray(raw.tiles) ? raw.tiles : []) + .map((item) => { + const x = integerOr(item?.x, NaN); + const y = integerOr(item?.y, NaN); + if (!Number.isFinite(x) || !Number.isFinite(y)) return null; + const util = Math.max(0, Math.min(100, numberOr(item?.utilizationPct, 0))); + return { + x, + y, + coord: String(item?.coord || `(${x},${y})`), + activeCycles: Math.max(0, integerOr(item?.activeCycles, 0)), + utilizationPct: util, + instCount: Math.max(0, integerOr(item?.instCount, 0)), + sendCount: Math.max(0, integerOr(item?.sendCount, 0)), + recvCount: Math.max(0, integerOr(item?.recvCount, 0)), + memoryCount: Math.max(0, integerOr(item?.memoryCount, 0)), + totalEvents: Math.max(0, integerOr(item?.totalEvents, 0)), + backpressureCount: Math.max(0, integerOr(item?.backpressureCount, 0)), + }; + }) + .filter(Boolean); + + const topHotTiles = (Array.isArray(raw.topHotTiles) ? raw.topHotTiles : []) + .map((item) => { + const x = integerOr(item?.x, NaN); + const y = integerOr(item?.y, NaN); + if (!Number.isFinite(x) || !Number.isFinite(y)) return null; + return { + x, + y, + coord: String(item?.coord || `(${x},${y})`), + utilizationPct: Math.max(0, Math.min(100, numberOr(item?.utilizationPct, 0))), + activeCycles: Math.max(0, integerOr(item?.activeCycles, 0)), + totalEvents: Math.max(0, integerOr(item?.totalEvents, 0)), + }; + }) + .filter(Boolean); + const topBackpressureTiles = (Array.isArray(raw.topBackpressureTiles) ? raw.topBackpressureTiles : []) + .map((item) => { + const x = integerOr(item?.x, NaN); + const y = integerOr(item?.y, NaN); + if (!Number.isFinite(x) || !Number.isFinite(y)) return null; + return { + x, + y, + coord: String(item?.coord || `(${x},${y})`), + backpressureCount: Math.max(0, integerOr(item?.backpressureCount, 0)), + }; + }) + .filter(Boolean); + + const gridWidth = Math.max(0, integerOr(raw?.grid?.width, 0)); + const gridHeight = Math.max(0, integerOr(raw?.grid?.height, 0)); + const activeTileCount = Math.max(0, integerOr(raw?.activeTileCount, tiles.length)); + const fallbackHot = [...tiles] + .sort((a, b) => { + if (b.utilizationPct !== a.utilizationPct) return b.utilizationPct - a.utilizationPct; + if (b.activeCycles !== a.activeCycles) return b.activeCycles - a.activeCycles; + return b.totalEvents - a.totalEvents; + }) + .slice(0, 8) + .map((t) => ({ + x: t.x, + y: t.y, + coord: t.coord, + utilizationPct: t.utilizationPct, + activeCycles: t.activeCycles, + totalEvents: t.totalEvents, + })); + const fallbackBackpressure = [...tiles] + .filter((t) => t.backpressureCount > 0) + .sort((a, b) => { + if (b.backpressureCount !== a.backpressureCount) return b.backpressureCount - a.backpressureCount; + if (b.totalEvents !== a.totalEvents) return b.totalEvents - a.totalEvents; + return b.activeCycles - a.activeCycles; + }) + .slice(0, 8) + .map((t) => ({ + x: t.x, + y: t.y, + coord: t.coord, + backpressureCount: t.backpressureCount, + })); + + return { + testName: String(raw.testName || ""), + logPath: String(raw.logPath || ""), + grid: { + width: gridWidth, + height: gridHeight, + }, + totalCycles: Math.max(0, integerOr(raw.totalCycles, 0)), + activeCyclesGlobal: Math.max(0, integerOr(raw.activeCyclesGlobal, 0)), + idleCyclesGlobal: Math.max(0, integerOr(raw.idleCyclesGlobal, 0)), + passed: nullableBool(raw.passed), + mismatchCount: raw.mismatchCount == null ? null : Math.max(0, integerOr(raw.mismatchCount, 0)), + instCount: Math.max(0, integerOr(raw.instCount, 0)), + sendCount: Math.max(0, integerOr(raw.sendCount, 0)), + recvCount: Math.max(0, integerOr(raw.recvCount, 0)), + memoryCount: Math.max(0, integerOr(raw.memoryCount, 0)), + totalEvents: Math.max(0, integerOr(raw.totalEvents, 0)), + backpressureCount: Math.max(0, integerOr(raw.backpressureCount, 0)), + backpressureCycles: Math.max(0, integerOr(raw.backpressureCycles, 0)), + activeTileCount, + tiles, + topHotTiles: topHotTiles.length > 0 ? topHotTiles : fallbackHot, + topBackpressureTiles: topBackpressureTiles.length > 0 ? topBackpressureTiles : fallbackBackpressure, + }; +} + +function formatPercent(v) { + if (!Number.isFinite(Number(v))) return "N/A"; + return `${Number(v).toFixed(1)}%`; +} + +function renderReportView() { + if (!controls.reportSummary || !controls.reportHotTiles || !controls.reportWarning) return; + + if (state.reportError) { + controls.reportWarning.textContent = state.reportError; + controls.reportWarning.className = "report-warning error"; + controls.reportSummary.innerHTML = "
Report parse failed. Please provide a valid report JSON.
"; + controls.reportHotTiles.innerHTML = ""; + return; + } + + if (!state.reportReady || !state.reportSpec) { + controls.reportWarning.textContent = "Load a report JSON to see aggregate utilization and hot-tile stats."; + controls.reportWarning.className = "report-warning"; + controls.reportSummary.innerHTML = "
No report loaded.
"; + controls.reportHotTiles.innerHTML = ""; + return; + } + + const report = state.reportSpec; + const cards = [ + ["test", report.testName || "N/A"], + ["passed", report.passed == null ? "N/A" : (report.passed ? "yes" : "no")], + ["mismatch", report.mismatchCount == null ? "N/A" : report.mismatchCount], + ["cycles", report.totalCycles], + ["active(global)", report.activeCyclesGlobal], + ["idle(global)", report.idleCyclesGlobal], + ["active-tiles", report.activeTileCount], + ["events", report.totalEvents], + ["bp-count", report.backpressureCount], + ["bp-cycles", report.backpressureCycles], + ]; + controls.reportSummary.innerHTML = cards.map( + ([k, v]) => `
${escapeHtml(k)}
${escapeHtml(v)}
`, + ).join(""); + + const meshW = state.maxX + 1; + const meshH = state.maxY + 1; + const reportW = integerOr(report.grid?.width, 0); + const reportH = integerOr(report.grid?.height, 0); + if (reportW > 0 && reportH > 0 && (reportW !== meshW || reportH !== meshH)) { + controls.reportWarning.textContent = + `grid mismatch: report=${reportW}x${reportH}, mesh=${meshW}x${meshH}. Heat overlay is clipped to current mesh.`; + controls.reportWarning.className = "report-warning warn"; + } else { + controls.reportWarning.textContent = `report loaded: ${reportW || "?"}x${reportH || "?"}, log=${report.logPath || "N/A"}`; + controls.reportWarning.className = "report-warning"; + } + + const hotTiles = (Array.isArray(report.topHotTiles) ? report.topHotTiles : []).slice(0, 12); + const bpTiles = (Array.isArray(report.topBackpressureTiles) ? report.topBackpressureTiles : []).slice(0, 12); + const sections = []; + if (hotTiles.length > 0) { + const rows = hotTiles.map((tile, idx) => + `${idx + 1}${escapeHtml(tile.coord || `(${tile.x},${tile.y})`)}${escapeHtml(formatPercent(tile.utilizationPct))}${escapeHtml(tile.activeCycles)}${escapeHtml(tile.totalEvents)}`).join(""); + sections.push([ + "
Top Hot Tiles
", + "", + "", + `${rows}`, + "
#coordutilizationactiveCyclesevents
", + ].join("")); + } else { + sections.push("
No hot-tile entries.
"); + } + if (bpTiles.length > 0) { + const bpRows = bpTiles.map((tile, idx) => + `${idx + 1}${escapeHtml(tile.coord || `(${tile.x},${tile.y})`)}${escapeHtml(tile.backpressureCount)}`).join(""); + sections.push([ + "
Top Backpressure Tiles
", + "", + "", + `${bpRows}`, + "
#coordbp-count
", + ].join("")); + } + controls.reportHotTiles.innerHTML = sections.join(""); +} + +function applyReportHeatOverlay() { + if (!staticLayer) return; + const heatTiles = staticLayer.selectAll(".tile-report-heat"); + if (!heatTiles || heatTiles.empty()) return; + + heatTiles + .style("display", "none") + .attr("opacity", 0); + + if (!state.reportReady || !state.reportSpec || state.reportHeatMetric !== "utilizationPct") return; + + const byCore = new Map(); + for (const tile of state.reportSpec.tiles || []) { + byCore.set(tileKey(tile.x, tile.y), Math.max(0, Math.min(100, numberOr(tile.utilizationPct, 0)))); + } + heatTiles.each(function (d) { + const k = tileKey(d.x, d.y); + if (!byCore.has(k)) return; + const util = byCore.get(k); + const alpha = 0.08 + (util / 100) * 0.52; + d3.select(this) + .style("display", null) + .attr("opacity", alpha) + .attr("data-util", util.toFixed(1)); + }); +} + +function loadReport(text) { + try { + state.reportSpec = parseReportJson(text); + state.reportReady = true; + state.reportError = ""; + if (!state.programSpec && state.events.length === 0) { + const rb = boundsFromReportSpec(state.reportSpec); + if (rb) { + state.maxX = rb.maxX; + state.maxY = rb.maxY; + applyAdaptiveLayout(); + drawStaticScene(); + } + } + applyReportHeatOverlay(); + renderReportView(); + } catch (err) { + state.reportSpec = null; + state.reportReady = false; + state.reportError = `Report JSON parse error: ${err.message}`; + applyReportHeatOverlay(); + renderReportView(); + } +} + +function abbrevOpLabel(slot, maxLen) { + const len = maxLen ?? 5; + const occTag = slot.occurrenceTotal > 1 ? `@${slot.sampleIndex}` : ""; + if (slot.opcode && String(slot.opcode).trim()) { + const s = String(slot.opcode).trim(); + const head = s.length <= len ? s : s.slice(0, len); + return `${head}${occTag}`; + } + return `#${slot.opId}${occTag}`; +} + +function weightedMedian(samples) { + if (!samples || samples.length === 0) return null; + const sorted = [...samples] + .filter((s) => Number.isFinite(s.value) && Number.isFinite(s.weight) && s.weight > 0) + .sort((a, b) => a.value - b.value); + if (sorted.length === 0) return null; + const total = sorted.reduce((acc, s) => acc + s.weight, 0); + let accWeight = 0; + for (const s of sorted) { + accWeight += s.weight; + if (accWeight >= total / 2) return s.value; + } + return sorted[sorted.length - 1].value; +} + +function boundaryLabel(x, y, bounds) { + const tags = []; + if (y === bounds.maxY) tags.push("N"); + if (y === bounds.minY) tags.push("S"); + if (x === bounds.minX) tags.push("W"); + if (x === bounds.maxX) tags.push("E"); + return tags.length > 0 ? tags.join("") : "Inner"; +} + +function computeDeltaRebased(rawDelta, corePhaseOffset, ii) { + if (!Number.isFinite(rawDelta) || !Number.isFinite(corePhaseOffset)) return null; + return signedDelta(rawDelta - corePhaseOffset, 0, ii); +} + +function summarizeTimingCell(items) { + const statusCounts = { onTime: 0, early: 0, late: 0, missing: 0 }; + let hasFirstDivergence = false; + let propagatedCount = 0; + let maxAbsDelta = 0; + for (const item of items) { + if (item.status === "on-time") statusCounts.onTime += 1; + if (item.status === "early") statusCounts.early += 1; + if (item.status === "late") statusCounts.late += 1; + if (item.status === "missing") statusCounts.missing += 1; + if (item.firstDivergence) hasFirstDivergence = true; + if (item.propagated) propagatedCount += 1; + if (Number.isFinite(item.delta)) { + maxAbsDelta = Math.max(maxAbsDelta, Math.abs(Number(item.delta))); + } + } + const anomalyCount = statusCounts.early + statusCounts.late + statusCounts.missing; + const anomalyScore = + statusCounts.missing * 4 + + statusCounts.late * 3 + + statusCounts.early * 2 + + (hasFirstDivergence ? 2 : 0) + + (propagatedCount > 0 ? 1 : 0); + let dominantStatus = "on-time"; + if (statusCounts.missing > 0) dominantStatus = "missing"; + else if (statusCounts.late > 0) dominantStatus = "late"; + else if (statusCounts.early > 0) dominantStatus = "early"; + return { + statusCounts, + dominantStatus, + anomalyCount, + anomalyScore, + hasAnomaly: anomalyCount > 0, + hasFirstDivergence, + opCount: items.length, + maxAbsDelta, + }; +} + +function buildTimingHeatmap(view) { + const cells = new Map(); + let maxScore = 1; + for (const c of view.columns) { + for (const slot of view.slots) { + const cellKey = `${c.coreKey}|${slot}`; + const items = view.cellMap.get(cellKey) || []; + const summary = summarizeTimingCell(items); + cells.set(cellKey, { + cellKey, + coreKey: c.coreKey, + x: c.x, + y: c.y, + slot, + ...summary, + }); + maxScore = Math.max(maxScore, summary.anomalyScore); + } + } + return { cells, maxScore }; +} + +function buildPhaseExplain(view) { + const xs = view.columns.map((c) => c.x); + const ys = view.columns.map((c) => c.y); + const bounds = { + minX: xs.length > 0 ? Math.min(...xs) : 0, + maxX: xs.length > 0 ? Math.max(...xs) : 0, + minY: ys.length > 0 ? Math.min(...ys) : 0, + maxY: ys.length > 0 ? Math.max(...ys) : 0, + }; + const coreMap = new Map(); + const boundarySamples = []; + const innerSamples = []; + + for (const c of view.columns) { + const label = boundaryLabel(c.x, c.y, bounds); + const isBoundary = label !== "Inner"; + const phaseOffset = Number.isFinite(c.phaseOffset) ? c.phaseOffset : null; + const confidence = Number.isFinite(c.phaseConfidence) ? c.phaseConfidence : 0; + const detail = { + isBoundary, + boundaryLabel: label, + phaseOffset, + phaseConfidence: confidence, + modeCount: c.modeCount, + }; + coreMap.set(c.coreKey, detail); + if (phaseOffset == null) continue; + const sample = { value: phaseOffset, weight: Math.max(1, c.modeCount || 0) }; + if (isBoundary) { + boundarySamples.push(sample); + } else { + innerSamples.push(sample); + } + } + + const boundaryPhase = weightedMedian(boundarySamples); + const innerPhase = weightedMedian(innerSamples); + const phaseGap = Number.isFinite(boundaryPhase) && Number.isFinite(innerPhase) + ? signedDelta(boundaryPhase, innerPhase, view.ii) + : null; + + return { + coreMap, + boundaryPhase, + innerPhase, + phaseGap, + }; +} + +function inferIngressSidesFromTrace(events) { + const sides = new Set(); + for (const e of events) { + if (e.msg !== "DataFlow" || e.Behavior !== "FeedIn" || !e.To) continue; + const ep = parseEndpoint(e.To); + if (ep && ep.kind === "tilePort") { + sides.add(ep.port); + } + } + if (sides.size === 0) { + sides.add("North"); + sides.add("West"); + } + return [...sides]; +} + +function distanceToIngress(x, y, bounds, ingressSides) { + const d = []; + for (const side of ingressSides) { + if (side === "North") d.push(bounds.maxY - y); + if (side === "South") d.push(y - bounds.minY); + if (side === "West") d.push(x - bounds.minX); + if (side === "East") d.push(bounds.maxX - x); + } + if (d.length === 0) return 0; + // In GEMM-like wavefronts, readiness is dominated by the slower upstream stream. + return Math.max(...d); +} + +function statusFromDelta(deltaValue, missing) { + if (missing) return "missing"; + if (!Number.isFinite(deltaValue)) return "missing"; + if (deltaValue === 0) return "on-time"; + return deltaValue < 0 ? "early" : "late"; +} + +function getModelOffset(modelItem, compModel) { + if (!modelItem) return null; + if (compModel === "distance") return modelItem.distanceOffset; + if (compModel === "fitted") return modelItem.fittedOffset; + return modelItem.hybridOffset; +} + +function getCompDeltaByModel(slot, compModel) { + if (compModel === "distance") return slot.deltaCompDistance; + if (compModel === "fitted") return slot.deltaCompFitted; + return slot.deltaCompHybrid; +} + +function getCompStatusByModel(slot, compModel) { + if (compModel === "distance") return slot.statusCompDistance; + if (compModel === "fitted") return slot.statusCompFitted; + return slot.statusCompHybrid; +} + +function summarizeModelBoundary(ii, phaseExplain, coreOffsets, modelKey) { + const boundarySamples = []; + const innerSamples = []; + for (const [coreKey, offsetInfo] of coreOffsets.entries()) { + const offset = offsetInfo[modelKey]; + if (!Number.isFinite(offset)) continue; + const meta = phaseExplain.coreMap.get(coreKey); + const weight = Math.max(1, Number(meta?.modeCount || 0)); + const sample = { value: Number(offset), weight }; + if (meta?.isBoundary) boundarySamples.push(sample); + else innerSamples.push(sample); + } + const boundary = weightedMedian(boundarySamples); + const inner = weightedMedian(innerSamples); + const gap = Number.isFinite(boundary) && Number.isFinite(inner) + ? signedDelta(boundary, inner, ii) + : null; + return { boundary, inner, gap }; +} + +function buildCompensationModels(view, phaseExplain, events) { + const ingressSides = inferIngressSidesFromTrace(events); + const xs = view.columns.map((c) => c.x); + const ys = view.columns.map((c) => c.y); + const bounds = { + minX: xs.length > 0 ? Math.min(...xs) : 0, + maxX: xs.length > 0 ? Math.max(...xs) : 0, + minY: ys.length > 0 ? Math.min(...ys) : 0, + maxY: ys.length > 0 ? Math.max(...ys) : 0, + }; + + const rawDistancePhaseSamples = []; + for (const c of view.columns) { + const dist = distanceToIngress(c.x, c.y, bounds, ingressSides); + const phase = view.ii > 0 ? normalizeSlot(dist, view.ii) : dist; + rawDistancePhaseSamples.push({ value: phase, weight: 1, coreKey: c.coreKey, rawDist: dist }); + } + const center = weightedMedian(rawDistancePhaseSamples); + const coreOffsets = new Map(); + for (const c of view.columns) { + const phaseMeta = phaseExplain.coreMap.get(c.coreKey) || {}; + const row = rawDistancePhaseSamples.find((v) => v.coreKey === c.coreKey); + const rawPhase = row ? row.value : 0; + const distanceOffset = view.ii > 0 + ? signedDelta(rawPhase, Number.isFinite(center) ? center : 0, view.ii) + : rawPhase - (Number.isFinite(center) ? center : 0); + const fittedOffset = Number.isFinite(phaseMeta.phaseOffset) ? Number(phaseMeta.phaseOffset) : null; + const fittedConfidence = Number.isFinite(phaseMeta.phaseConfidence) ? Number(phaseMeta.phaseConfidence) : 0; + const hybridOffset = (Number.isFinite(fittedOffset) && fittedConfidence >= 0.4) + ? fittedOffset + : distanceOffset; + coreOffsets.set(c.coreKey, { + distanceOffset, + fittedOffset, + hybridOffset, + fittedConfidence, + ingressDistance: row ? row.rawDist : 0, + }); + } + + return { + ingressSides, + coreOffsets, + models: { + distance: summarizeModelBoundary(view.ii, phaseExplain, coreOffsets, "distanceOffset"), + fitted: summarizeModelBoundary(view.ii, phaseExplain, coreOffsets, "fittedOffset"), + hybrid: summarizeModelBoundary(view.ii, phaseExplain, coreOffsets, "hybridOffset"), + }, + }; +} + +function alignSlotAtOrAfter(startTime, expectedSlot, ii) { + const t0 = Math.round(Number(startTime || 0)); + if (ii <= 0) return expectedSlot; + const slot = normalizeSlot(expectedSlot, ii); + let offset = slot - normalizeSlot(t0, ii); + if (offset < 0) offset += ii; + return t0 + offset; +} + +function buildTimelineLanes(view, visibleColumns, phaseExplain, compensation) { + const lanes = []; + let minT = Number.POSITIVE_INFINITY; + let maxT = Number.NEGATIVE_INFINITY; + let totalSlots = 0; + + for (const c of visibleColumns) { + const coreMeta = phaseExplain.coreMap.get(c.coreKey) || {}; + const expectedSlots = []; + const actualSlots = []; + const compMeta = compensation.coreOffsets.get(c.coreKey) || null; + for (const item of c.items) { + const samples = (Array.isArray(item.allSamples) && item.allSamples.length > 0) + ? item.allSamples + : [null]; + const occurrenceTotal = samples.length; + for (let sampleIdx = 0; sampleIdx < samples.length; sampleIdx += 1) { + const sample = samples[sampleIdx]; + const hasActual = sample && Number.isFinite(sample.time); + const actualTime = hasActual ? Math.round(sample.time) : null; + const deltaStrict = hasActual ? sample.delta : item.delta; + const statusStrict = hasActual ? sample.status : item.status; + const missing = !hasActual; + + let expectedTime = null; + if (hasActual && Number.isFinite(deltaStrict)) { + // Expand expected per actual occurrence to cover full trace length. + expectedTime = Math.round(actualTime - deltaStrict); + } else if (Number.isFinite(item.firstTime) && Number.isFinite(item.delta)) { + expectedTime = Math.round(item.firstTime - item.delta); + } else if (view.ii > 0) { + expectedTime = alignSlotAtOrAfter(state.minTime, item.expectedSlot, view.ii); + } else { + expectedTime = item.expectedSlot; + } + + const deltaRebased = computeDeltaRebased(deltaStrict, coreMeta.phaseOffset, view.ii); + const deltaCompDistance = computeDeltaRebased(deltaStrict, compMeta?.distanceOffset, view.ii); + const deltaCompFitted = computeDeltaRebased(deltaStrict, compMeta?.fittedOffset, view.ii); + const deltaCompHybrid = computeDeltaRebased(deltaStrict, compMeta?.hybridOffset, view.ii); + const statusCompDistance = statusFromDelta(deltaCompDistance, missing); + const statusCompFitted = statusFromDelta(deltaCompFitted, missing); + const statusCompHybrid = statusFromDelta(deltaCompHybrid, missing); + const cellKey = `${c.coreKey}|${item.expectedSlot}`; + const slot = { + coreKey: c.coreKey, + x: c.x, + y: c.y, + expectedSlot: item.expectedSlot, + opId: item.id, + opcode: item.opcode || "", + status: statusStrict, + statusStrict, + expectedTime, + actualTime, + delta: deltaStrict, + deltaStrict, + deltaRebased, + deltaCompDistance, + deltaCompFitted, + deltaCompHybrid, + statusCompDistance, + statusCompFitted, + statusCompHybrid, + compDistanceOffset: compMeta?.distanceOffset ?? null, + compFittedOffset: compMeta?.fittedOffset ?? null, + compHybridOffset: compMeta?.hybridOffset ?? null, + firstDivergence: item.firstDivergence, + propagated: item.propagated, + cellKey, + sampleIdx, + sampleIndex: sampleIdx + 1, + occurrenceTotal, + samplePred: hasActual ? sample.pred : null, + sampleSource: hasActual ? String(sample.source || "Unknown") : null, + }; + expectedSlots.push(slot); + if (Number.isFinite(actualTime)) { + actualSlots.push(slot); + } + + if (Number.isFinite(expectedTime)) { + minT = Math.min(minT, expectedTime); + maxT = Math.max(maxT, expectedTime); + totalSlots += 1; + } + if (Number.isFinite(actualTime)) { + minT = Math.min(minT, actualTime); + maxT = Math.max(maxT, actualTime); + totalSlots += 1; + } + } + } + lanes.push({ + coreKey: c.coreKey, + x: c.x, + y: c.y, + modeDelta: c.modeDelta, + modeCount: c.modeCount, + statusCounts: c.statusCounts, + phaseOffset: coreMeta.phaseOffset ?? null, + phaseConfidence: coreMeta.phaseConfidence ?? 0, + compDistanceOffset: compMeta?.distanceOffset ?? null, + compFittedOffset: compMeta?.fittedOffset ?? null, + compHybridOffset: compMeta?.hybridOffset ?? null, + compFittedConfidence: compMeta?.fittedConfidence ?? 0, + boundaryLabel: coreMeta.boundaryLabel || "Inner", + isBoundary: Boolean(coreMeta.isBoundary), + expectedSlots, + actualSlots, + }); + } + + if (!Number.isFinite(minT) || !Number.isFinite(maxT)) { + minT = state.minTime; + maxT = state.maxTime; + } + minT = Math.min(minT, state.minTime); + maxT = Math.max(maxT, state.maxTime); + if (minT === maxT) maxT = minT + 1; + + return { + lanes, + timeMin: minT, + timeMax: maxT, + totalSlots, + }; +} + +function tickStepByRange(span) { + if (span <= 40) return 2; + if (span <= 120) return 5; + if (span <= 360) return 10; + if (span <= 900) return 25; + return 50; +} + +function renderTimelineSvg(_view, timeline) { + const wrap = controls.timingGrid; + if (!wrap) return; + wrap.innerHTML = ""; + + const baselineView = ["strict", "compensated", "split"].includes(state.timingBaselineView) + ? state.timingBaselineView + : "strict"; + const compModel = ["distance", "fitted", "hybrid"].includes(state.timingCompModel) + ? state.timingCompModel + : "hybrid"; + + const fullMin = timeline.timeMin; + const fullMax = timeline.timeMax; + const fullSpan = Math.max(1, fullMax - fullMin + 1); + const minWindow = 1; + const windowSize = clamp( + Math.round(Number(state.timingWindowSize || Math.min(120, fullSpan))), + minWindow, + fullSpan, + ); + const startMax = Math.max(fullMin, fullMax - windowSize + 1); + const windowStart = clamp( + Math.round(Number(state.timingWindowStart || fullMin)), + fullMin, + startMax, + ); + const windowEnd = windowStart + windowSize - 1; + state.timingWindowStart = windowStart; + state.timingWindowSize = windowSize; + + if (controls.timingWindowStart) { + controls.timingWindowStart.min = String(fullMin); + controls.timingWindowStart.max = String(startMax); + controls.timingWindowStart.value = String(windowStart); + controls.timingWindowStart.disabled = fullSpan <= 1; + } + if (controls.timingWindowSize) { + controls.timingWindowSize.min = String(minWindow); + controls.timingWindowSize.max = String(fullSpan); + controls.timingWindowSize.value = String(windowSize); + } + if (controls.timingWindowStartLabel) { + controls.timingWindowStartLabel.textContent = `T${windowStart}-T${windowEnd}`; + } + if (controls.timingWindowSizeLabel) { + controls.timingWindowSizeLabel.textContent = `${windowSize} cycles`; + } + + const zoomY = clamp(Number(state.timingZoomY || 1), 0.6, 4); + state.timingZoomX = 1; + state.timingZoomY = zoomY; + if (controls.timingZoomY) { + controls.timingZoomY.value = String(Math.round(zoomY * 100)); + } + if (controls.timingZoomYLabel) { + controls.timingZoomYLabel.textContent = `${zoomY.toFixed(2)}x`; + } + + const leftPad = 242; + const rightPad = 30; + const topPad = 30; + const bottomPad = 38; + const slotHeight = clamp(Math.round(8 * zoomY), 6, 34); + const subLaneGap = clamp(Math.round(5 * zoomY), 3, 22); + const laneGap = clamp(Math.round(10 * zoomY), 6, 46); + const splitView = baselineView === "split"; + const baseLaneRows = splitView ? 3 : 2; + const availableCoreKeys = new Set(timeline.lanes.map((lane) => lane.coreKey)); + const selectedIoKeys = new Set([...(state.timingIoWaveExpandedCoreKeys || [])]); + const ioWaveExpandedKeys = state.timingIoWaveExpandAll + ? new Set([...availableCoreKeys]) + : new Set([...selectedIoKeys].filter((key) => availableCoreKeys.has(key))); + const rowStep = slotHeight + subLaneGap; + const laneData = []; + let yCursor = topPad; + for (let idx = 0; idx < timeline.lanes.length; idx += 1) { + const lane = timeline.lanes[idx]; + const hasIoRows = ioWaveExpandedKeys.has(lane.coreKey); + const laneRows = baseLaneRows + (hasIoRows ? 2 : 0); + const laneHeight = laneRows * slotHeight + (laneRows - 1) * subLaneGap; + laneData.push({ + ...lane, + idx, + hasIoRows, + laneRows, + yBase: yCursor, + yExpected: yCursor, + yStrict: yCursor + rowStep, + yComp: yCursor + rowStep * 2, + yIoIn: hasIoRows ? yCursor + rowStep * baseLaneRows : null, + yIoOut: hasIoRows ? yCursor + rowStep * (baseLaneRows + 1) : null, + }); + yCursor += laneHeight + laneGap; + } + const laneCount = Math.max(1, laneData.length); + const plotH = Math.max(1, yCursor - topPad - laneGap); + const wrapWidth = Math.max(860, Math.round(wrap.clientWidth || 0) - 2); + const plotW = Math.max(620, wrapWidth - leftPad - rightPad); + const width = leftPad + plotW + rightPad; + const height = topPad + plotH + bottomPad; + const labelFontSize = clamp(Math.round(slotHeight * 0.72), 7, 13); + const labelMinWidth = Math.max(14, labelFontSize * 2 + 2); + + const svgEl = d3.create("svg") + .attr("id", "timingTimelineSvg") + .attr("class", "timing-timeline-svg") + .attr("viewBox", `0 0 ${width} ${height}`) + .attr("width", width) + .attr("height", height); + + const xScale = d3.scaleLinear() + .domain([windowStart, windowEnd + 1]) + .range([leftPad, leftPad + plotW]); + state.timingViewport = { + fullMin, + fullMax, + fullSpan, + windowStart, + windowSize, + windowEnd, + leftPad, + plotW, + }; + + const ticks = []; + for (let t = windowStart; t <= windowEnd; t += 1) ticks.push(t); + + // Cycle boundaries (X): dashed vertical lines only; no other grid + const grid = svgEl.append("g").attr("class", "timeline-grid"); + for (const t of ticks) { + const x = xScale(t); + grid.append("line") + .attr("x1", x).attr("x2", x) + .attr("y1", topPad).attr("y2", topPad + plotH) + .attr("class", "timeline-cycle-sep"); + grid.append("text") + .attr("x", x + 2) + .attr("y", topPad + plotH + 16) + .attr("class", "timeline-tick") + .text(`T${t}`); + } + svgEl.append("line") + .attr("x1", leftPad).attr("x2", leftPad + plotW) + .attr("y1", topPad + plotH).attr("y2", topPad + plotH) + .attr("class", "timeline-axis"); + + // Core boundaries (Y): one dashed/dark line between each core for easier row matching + const coreSep = svgEl.append("g").attr("class", "timeline-core-seps"); + for (let idx = 1; idx < laneCount; idx += 1) { + const y = laneData[idx].yBase; + coreSep.append("line") + .attr("x1", leftPad) + .attr("x2", leftPad + plotW) + .attr("y1", y) + .attr("y2", y) + .attr("class", "timeline-core-sep"); + } + + // Lane labels (no inner sub-row grid lines) + const lanesG = svgEl.append("g").attr("class", "timeline-lanes"); + for (const lane of laneData) { + const modelOffset = compModel === "distance" + ? lane.compDistanceOffset + : (compModel === "fitted" ? lane.compFittedOffset : lane.compHybridOffset); + const phaseText = state.showPhaseExplain + ? ` SΔ=${lane.phaseOffset == null ? "N/A" : formatDelta(lane.phaseOffset)} CΔ=${formatDelta(modelOffset)}` + : ""; + lanesG.append("text") + .attr("x", 8) + .attr("y", lane.yExpected + slotHeight + 1) + .attr("class", [ + "timeline-core-label", + lane.isBoundary ? "boundary" : "", + lane.hasIoRows ? "io-expanded" : "", + state.timingFocusedCoreKey === lane.coreKey ? "focused" : "", + ].filter(Boolean).join(" ")) + .attr("data-core-key", lane.coreKey) + .attr("title", `Click: focus core (${lane.x},${lane.y}) | Double-click: toggle IO wave`) + .text(`(${lane.x},${lane.y}) ${lane.boundaryLabel}${phaseText}`); + lanesG.append("text") + .attr("x", leftPad - 64) + .attr("y", lane.yExpected + slotHeight - 1) + .attr("class", "timeline-lane-tag") + .text("E"); + lanesG.append("text") + .attr("x", leftPad - 64) + .attr("y", lane.yStrict + slotHeight - 1) + .attr("class", "timeline-lane-tag") + .text(splitView ? "S" : (baselineView === "strict" ? "A" : "C")); + if (splitView) { + lanesG.append("text") + .attr("x", leftPad - 64) + .attr("y", lane.yComp + slotHeight - 1) + .attr("class", "timeline-lane-tag") + .text("C"); + } + if (lane.hasIoRows) { + lanesG.append("text") + .attr("x", leftPad - 64) + .attr("y", lane.yIoIn + slotHeight - 1) + .attr("class", "timeline-lane-tag timeline-io-tag-in") + .text("IN"); + lanesG.append("text") + .attr("x", leftPad - 64) + .attr("y", lane.yIoOut + slotHeight - 1) + .attr("class", "timeline-lane-tag timeline-io-tag-out") + .text("OUT"); + } + } + + const slotG = svgEl.append("g").attr("class", "timeline-rects"); + const keepSlot = (slot) => { + if (!state.timingAnomalyOnly) return true; + const strictAnomaly = slot.statusStrict !== "on-time"; + const compAnomaly = getCompStatusByModel(slot, compModel) !== "on-time"; + if (baselineView === "strict") return strictAnomaly; + if (baselineView === "compensated") return compAnomaly; + return strictAnomaly || compAnomaly; + }; + const applyStackLayout = (items, keyOf, tieBreakOf) => { + const buckets = new Map(); + for (const item of items) { + const key = keyOf(item); + const arr = buckets.get(key) || []; + arr.push(item); + buckets.set(key, arr); + } + for (const group of buckets.values()) { + group.sort((a, b) => tieBreakOf(a) - tieBreakOf(b)); + const total = group.length; + if (total <= 1) { + group[0].stackIndex = 0; + group[0].stackTotal = 1; + continue; + } + for (let i = 0; i < group.length; i += 1) { + group[i].stackIndex = i; + group[i].stackTotal = total; + } + } + }; + const resolveStackGeometry = (baseY, baseH, stackIndex, stackTotal) => { + if (!Number.isFinite(stackTotal) || stackTotal <= 1) { + return { y: baseY, h: baseH }; + } + // Keep all stacked blocks visible within one cycle slot row. + const gap = 1; + const innerH = Math.max(2, Math.floor((baseH - gap * (stackTotal - 1)) / stackTotal)); + const y = baseY + stackIndex * (innerH + gap); + return { y, h: innerH }; + }; + const summarizeWaveValues = (values, maxItems = 2) => { + const arr = Array.isArray(values) ? values : []; + if (arr.length === 0) return ""; + const shown = arr.slice(0, maxItems).map((v) => shortText(v, 7)); + const remain = arr.length - shown.length; + if (remain > 0) shown.push(`+${remain}`); + return shown.join(","); + }; + const ioBusPath = (xLeft, xRight, yTop, yBottom) => { + const yMid = (yTop + yBottom) / 2; + const w = Math.max(1, xRight - xLeft); + const edge = Math.min(5, Math.max(1, Math.round(w * 0.22))); + return [ + `M${xLeft + edge},${yTop}`, + `L${xRight - edge},${yTop}`, + `L${xRight},${yMid}`, + `L${xRight - edge},${yBottom}`, + `L${xLeft + edge},${yBottom}`, + `L${xLeft},${yMid}`, + "Z", + ].join(" "); + }; + const drawIoWaveRow = (lane, yPos, direction) => { + if (!Number.isFinite(yPos)) return; + const byTime = state.coreIoWaveByTime.get(lane.coreKey); + if (!byTime) return; + for (let t = windowStart; t <= windowEnd; t += 1) { + const entry = byTime.get(t); + const values = direction === "in" ? entry?.inVals : entry?.outVals; + if (!Array.isArray(values) || values.length === 0) continue; + const xLeft = xScale(t); + const xRight = xScale(t + 1); + const w = Math.max(1, xRight - xLeft); + slotG.append("path") + .attr("d", ioBusPath(xLeft, xRight, yPos, yPos + slotHeight)) + .attr("class", `timeline-io-bus ${direction === "in" ? "timeline-io-bus-in" : "timeline-io-bus-out"}`) + .attr( + "title", + `${direction === "in" ? "Input" : "Output"} core=(${lane.x},${lane.y}) t=${t} values=${values.join(",")}`, + ); + if (w >= labelMinWidth + 6) { + slotG.append("text") + .attr("x", xLeft + w / 2) + .attr("y", yPos + slotHeight / 2) + .attr("text-anchor", "middle") + .attr("dominant-baseline", "middle") + .attr("class", `timeline-io-bus-label ${direction === "in" ? "timeline-io-bus-label-in" : "timeline-io-bus-label-out"}`) + .attr("font-size", labelFontSize) + .text(summarizeWaveValues(values)); + } + } + }; + const drawActualRow = (lane, yPos, rowMode) => { + const drawables = []; + for (const slot of lane.expectedSlots) { + if (!keepSlot(slot)) continue; + const rowStatus = rowMode === "strict" ? slot.statusStrict : getCompStatusByModel(slot, compModel); + let drawTime = null; + let cls = "actual-ok"; + if (rowStatus === "missing") { + drawTime = slot.expectedTime; + cls = "missing"; + } else if (Number.isFinite(slot.actualTime)) { + drawTime = slot.actualTime; + if (rowMode === "strict") { + cls = rowStatus === "on-time" ? "actual-ok" : "actual-bad"; + } else { + cls = rowStatus === "on-time" ? "actual-comp-ok" : "actual-comp-bad"; + } + } + if (!Number.isFinite(drawTime)) continue; + if (drawTime < windowStart || drawTime > windowEnd) continue; + drawables.push({ slot, rowStatus, drawTime, cls }); + } + applyStackLayout( + drawables, + (d) => `${lane.coreKey}|${rowMode}|${d.drawTime}`, + (d) => (d.slot.opId * 10000) + (d.slot.sampleIndex || 0), + ); + for (const d of drawables) { + const { slot, rowStatus, drawTime, cls, stackIndex = 0, stackTotal = 1 } = d; + const x0 = xScale(drawTime); + const x1 = xScale(drawTime + 1); + const w = Math.max(1, Math.floor(x1 - x0 - 1)); + const selected = state.timingSelectedCell === slot.cellKey ? "selected" : ""; + const geom = resolveStackGeometry(yPos, slotHeight, stackIndex, stackTotal); + slotG.append("rect") + .attr("x", x0 + 0.5) + .attr("y", geom.y) + .attr("width", w) + .attr("height", geom.h) + .attr("class", `timeline-rect ${cls} ${selected}`) + .attr("data-timing-cell", slot.cellKey) + .attr( + "title", + `${rowMode === "strict" ? "Strict" : `Comp(${compModel})`} #${slot.opId}[${slot.sampleIndex}/${slot.occurrenceTotal}] status=${rowStatus} t=${Number.isFinite(slot.actualTime) ? slot.actualTime : "N/A"} deltaS=${formatDelta(slot.deltaStrict)} deltaC=${formatDelta(getCompDeltaByModel(slot, compModel))}`, + ); + if (w >= labelMinWidth && geom.h >= 8) { + slotG.append("text") + .attr("x", x0 + 0.5 + w / 2) + .attr("y", geom.y + geom.h / 2) + .attr("text-anchor", "middle") + .attr("dominant-baseline", "middle") + .attr("class", "timeline-rect-label timeline-rect-label-actual") + .attr("font-size", labelFontSize) + .text(abbrevOpLabel(slot)); + } + + if (state.timingSelectedCell === slot.cellKey && Number.isFinite(slot.actualTime) && Number.isFinite(slot.expectedTime)) { + const xe = xScale(slot.expectedTime) + Math.max(1, Math.floor((xScale(slot.expectedTime + 1) - xScale(slot.expectedTime)) / 2)); + const xa = xScale(slot.actualTime) + Math.max(1, Math.floor((xScale(slot.actualTime + 1) - xScale(slot.actualTime)) / 2)); + const linkClass = rowMode === "strict" + ? (rowStatus === "on-time" ? "ok" : "bad") + : (rowStatus === "on-time" ? "comp-ok" : "comp-bad"); + slotG.append("line") + .attr("x1", xe).attr("y1", lane.yExpected + slotHeight) + .attr("x2", xa).attr("y2", geom.y) + .attr("class", `timeline-link ${linkClass}`); + } + } + }; + + for (const lane of laneData) { + const expectedDrawables = []; + for (const slot of lane.expectedSlots) { + if (!keepSlot(slot)) continue; + if (!Number.isFinite(slot.expectedTime)) continue; + if (slot.expectedTime < windowStart || slot.expectedTime > windowEnd) continue; + expectedDrawables.push({ slot, drawTime: slot.expectedTime }); + } + applyStackLayout( + expectedDrawables, + (d) => `${lane.coreKey}|expected|${d.drawTime}`, + (d) => (d.slot.opId * 10000) + (d.slot.sampleIndex || 0), + ); + for (const d of expectedDrawables) { + const { slot, stackIndex = 0, stackTotal = 1 } = d; + const x0 = xScale(slot.expectedTime); + const x1 = xScale(slot.expectedTime + 1); + const w = Math.max(1, Math.floor(x1 - x0 - 1)); + const selected = state.timingSelectedCell === slot.cellKey ? "selected" : ""; + const geom = resolveStackGeometry(lane.yExpected, slotHeight, stackIndex, stackTotal); + slotG.append("rect") + .attr("x", x0 + 0.5) + .attr("y", geom.y) + .attr("width", w) + .attr("height", geom.h) + .attr("class", `timeline-rect expected ${selected}`) + .attr("data-timing-cell", slot.cellKey) + .attr( + "title", + `Expected #${slot.opId}[${slot.sampleIndex}/${slot.occurrenceTotal}] (${slot.opcode || "N/A"}) t=${slot.expectedTime} deltaS=${formatDelta(slot.deltaStrict)} deltaC=${formatDelta(getCompDeltaByModel(slot, compModel))}`, + ); + if (w >= labelMinWidth && geom.h >= 8) { + slotG.append("text") + .attr("x", x0 + 0.5 + w / 2) + .attr("y", geom.y + geom.h / 2) + .attr("text-anchor", "middle") + .attr("dominant-baseline", "middle") + .attr("class", "timeline-rect-label timeline-rect-label-expected") + .attr("font-size", labelFontSize) + .text(abbrevOpLabel(slot)); + } + } + if (baselineView === "strict") { + drawActualRow(lane, lane.yStrict, "strict"); + } else if (baselineView === "compensated") { + drawActualRow(lane, lane.yStrict, "comp"); + } else { + drawActualRow(lane, lane.yStrict, "strict"); + drawActualRow(lane, lane.yComp, "comp"); + } + if (lane.hasIoRows) { + drawIoWaveRow(lane, lane.yIoIn, "in"); + drawIoWaveRow(lane, lane.yIoOut, "out"); + } + } + + // Legend + const legend = svgEl.append("g").attr("class", "timeline-legend").attr("transform", `translate(${leftPad},14)`); + const legendItems = baselineView === "strict" + ? [ + ["Expected slot", "timeline-legend-exp"], + ["Strict on-time", "timeline-legend-act-ok"], + ["Strict mismatch", "timeline-legend-act-bad"], + ["Missing", "timeline-legend-missing"], + ] + : (baselineView === "compensated" + ? [ + ["Expected slot", "timeline-legend-exp"], + [compModel === "hybrid" ? "Hybrid on-time" : `Comp(${compModel}) on-time`, "timeline-legend-comp-ok"], + [compModel === "hybrid" ? "Hybrid mismatch" : `Comp(${compModel}) mismatch`, "timeline-legend-comp-bad"], + ["Missing", "timeline-legend-missing"], + ] + : [ + ["Expected slot", "timeline-legend-exp"], + ["Strict on-time", "timeline-legend-act-ok"], + ["Strict mismatch", "timeline-legend-act-bad"], + [`Comp(${compModel}) on-time`, "timeline-legend-comp-ok"], + [`Comp(${compModel}) mismatch`, "timeline-legend-comp-bad"], + ["Missing", "timeline-legend-missing"], + ]); + if (laneData.some((lane) => lane.hasIoRows)) { + legendItems.push(["IN bus", "timeline-legend-io-in"]); + legendItems.push(["OUT bus", "timeline-legend-io-out"]); + } + const legendGap = 132; + legendItems.forEach((it, i) => { + const gx = i * legendGap; + legend.append("rect").attr("x", gx).attr("y", -4).attr("width", 10).attr("height", 8).attr("class", it[1]); + legend.append("text").attr("x", gx + 14).attr("y", 4).attr("class", "timeline-legend-text").text(it[0]); + }); + + wrap.appendChild(svgEl.node()); +} + +function timelineZoomAnchorTimeFromWheel(event) { + const vp = state.timingViewport; + if (!vp) { + return Number(state.timingWindowStart || 0) + Number(state.timingWindowSize || 1) / 2; + } + const svgElement = document.getElementById("timingTimelineSvg"); + if (!svgElement) { + return vp.windowStart + vp.windowSize / 2; + } + const rect = svgElement.getBoundingClientRect(); + const localX = event.clientX - rect.left; + const ratio = clamp((localX - vp.leftPad) / Math.max(1, vp.plotW), 0, 0.999); + return vp.windowStart + ratio * vp.windowSize; +} + +function handleTimelineCtrlWheelZoom(event) { + if (!event.ctrlKey) return; + if (state.events.length === 0 || !state.programSpec) return; + event.preventDefault(); + + const vp = state.timingViewport || { + fullMin: state.minTime, + fullMax: state.maxTime, + fullSpan: Math.max(1, state.maxTime - state.minTime + 1), + }; + const fullMin = vp.fullMin; + const fullMax = vp.fullMax; + const fullSpan = Math.max(1, vp.fullSpan || (fullMax - fullMin + 1)); + const minWindow = 1; + const oldWindow = Math.max(1, Number(state.timingWindowSize || Math.min(120, fullSpan))); + const oldStart = Number(state.timingWindowStart || fullMin); + const zoomIn = event.deltaY < 0; + const anchorTime = timelineZoomAnchorTimeFromWheel(event); + const nextWindow = clamp( + Math.round(oldWindow * (zoomIn ? 0.88 : 1.14)), + minWindow, + fullSpan, + ); + const anchorRatio = clamp((anchorTime - oldStart) / oldWindow, 0, 1); + const startMax = Math.max(fullMin, fullMax - nextWindow + 1); + const nextStart = clamp( + Math.round(anchorTime - anchorRatio * nextWindow), + fullMin, + startMax, + ); + const factor = zoomIn ? 1.08 : 1 / 1.08; + state.timingWindowStart = nextStart; + state.timingWindowSize = nextWindow; + state.timingZoomX = 1; + state.timingZoomY = clamp(Number(state.timingZoomY || 1) * factor, 0.6, 4); + renderTimingView(); +} + +function getTimelineSvgSize(svgElement) { + const vb = svgElement.getAttribute("viewBox"); + if (vb) { + const parts = vb.trim().split(/\s+/).map(Number); + if (parts.length === 4 && Number.isFinite(parts[2]) && Number.isFinite(parts[3])) { + return { width: parts[2], height: parts[3] }; + } + } + const width = Number(svgElement.getAttribute("width")) || svgElement.clientWidth || 1200; + const height = Number(svgElement.getAttribute("height")) || svgElement.clientHeight || 800; + return { width, height }; +} + +function parseMaxSide() { + const fallback = 4096; + if (!controls.timingExportMaxSide) return fallback; + const v = Math.round(Number(controls.timingExportMaxSide.value)); + if (!Number.isFinite(v)) return fallback; + return clamp(v, 512, 16000); +} + +function timelineExportCss() { + return ` +.timing-timeline-svg { background: #fffaf0; } +.timeline-axis { stroke: #8f846d; stroke-width: 1; } +.timeline-cycle-sep { stroke: #c4b89a; stroke-width: 1; stroke-dasharray: 3 2; } +.timeline-core-sep { stroke: #7a6f58; stroke-width: 1.2; stroke-dasharray: 4 3; } +.timeline-tick { fill: #7a6f58; font-size: 10px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } +.timeline-core-label { fill: #5a5347; font-size: 11px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } +.timeline-core-label.boundary { font-weight: 700; } +.timeline-core-label.focused { fill: #1f4eb5; font-weight: 700; text-decoration: underline; } +.timeline-core-label.io-expanded { fill: #6a2b96; text-decoration: underline; text-decoration-style: dashed; } +.timeline-lane-tag { fill: #7f7460; font-size: 10px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } +.timeline-io-tag-in { fill: #2d6cdf; font-weight: 700; } +.timeline-io-tag-out { fill: #8f2ac7; font-weight: 700; } +.timeline-rect.expected { fill: #f4f4f4; stroke: #8f8f8f; stroke-width: 0.8; } +.timeline-rect.actual-ok { fill: #2a7f62; stroke: #1f604a; stroke-width: 0.7; } +.timeline-rect.actual-bad { fill: #d62828; stroke: #8f1717; stroke-width: 0.7; } +.timeline-rect.actual-comp-ok { fill: #2d6cdf; stroke: #1d4a97; stroke-width: 0.8; opacity: 0.84; } +.timeline-rect.actual-comp-bad { fill: #9b2ce0; stroke: #5d178a; stroke-width: 0.85; opacity: 0.9; } +.timeline-rect.missing { fill: #f4f4f4; stroke: #7a7a7a; stroke-width: 1.1; stroke-dasharray: 2 1; } +.timeline-rect.selected { stroke-width: 1.8; } +.timeline-io-bus { stroke-width: 0.9; } +.timeline-io-bus-in { fill: #deebff; stroke: #7da3ea; } +.timeline-io-bus-out { fill: #f1e1ff; stroke: #b589dd; } +.timeline-io-bus-label { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; pointer-events: none; font-size: 7px; } +.timeline-io-bus-label-in { fill: #214a9c; } +.timeline-io-bus-label-out { fill: #6d2094; } +.timeline-missing { stroke: #7a7a7a; stroke-width: 1.2; } +.timeline-link.ok { stroke: rgba(54, 132, 103, 0.45); stroke-width: 0.9; } +.timeline-link.bad { stroke: rgba(214, 40, 40, 0.72); stroke-width: 1.2; } +.timeline-link.comp-ok { stroke: rgba(45, 108, 223, 0.5); stroke-width: 0.9; stroke-dasharray: 2 1; } +.timeline-link.comp-bad { stroke: rgba(155, 44, 224, 0.78); stroke-width: 1.2; stroke-dasharray: 2 1; } +.timeline-legend-text { fill: #615a4f; font-size: 10px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } +.timeline-legend-exp { fill: #fff; stroke: #8c8c8c; } +.timeline-legend-act-ok { fill: #2a7f62; } +.timeline-legend-act-bad { fill: #d62828; } +.timeline-legend-missing { fill: #f4f4f4; stroke: #7a7a7a; stroke-dasharray: 2 1; } +.timeline-legend-comp-ok { fill: #2d6cdf; } +.timeline-legend-comp-bad { fill: #9b2ce0; } +.timeline-legend-io-in { fill: #deebff; stroke: #7da3ea; } +.timeline-legend-io-out { fill: #f1e1ff; stroke: #b589dd; } +.timeline-rect-label { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 7px; } +.timeline-rect-label-expected { fill: #444; } +.timeline-rect-label-actual { fill: #fff; }`; +} + +function exportTimelinePng() { + const svgElement = document.getElementById("timingTimelineSvg"); + if (!svgElement) return; + const size = getTimelineSvgSize(svgElement); + const maxSide = parseMaxSide(); + const scale = Math.min(1, maxSide / Math.max(size.width, size.height)); + const outW = Math.max(1, Math.round(size.width * scale)); + const outH = Math.max(1, Math.round(size.height * scale)); + + const serializer = new XMLSerializer(); + const clone = svgElement.cloneNode(true); + const styleEl = document.createElementNS("http://www.w3.org/2000/svg", "style"); + styleEl.textContent = timelineExportCss(); + clone.insertBefore(styleEl, clone.firstChild); + let source = serializer.serializeToString(clone); + if (!source.includes("xmlns=\"http://www.w3.org/2000/svg\"")) { + source = source.replace(" { + const canvas = document.createElement("canvas"); + canvas.width = outW; + canvas.height = outH; + const ctx = canvas.getContext("2d"); + if (!ctx) { + URL.revokeObjectURL(url); + return; + } + ctx.fillStyle = "#fffdf7"; + ctx.fillRect(0, 0, outW, outH); + ctx.drawImage(img, 0, 0, outW, outH); + canvas.toBlob((blob) => { + if (!blob) { + URL.revokeObjectURL(url); + return; + } + const dlUrl = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = dlUrl; + a.download = "timeline.png"; + a.click(); + URL.revokeObjectURL(dlUrl); + URL.revokeObjectURL(url); + }, "image/png"); + }; + img.onerror = () => { + URL.revokeObjectURL(url); + }; + img.src = url; +} + +function splitCellKey(cellKey) { + const pivot = cellKey.lastIndexOf("|"); + if (pivot <= 0) return { coreKey: "", slot: 0 }; + return { + coreKey: cellKey.slice(0, pivot), + slot: Number(cellKey.slice(pivot + 1)), + }; +} + +function buildCoreIoWaveByTime(events) { + const byCore = new Map(); + const ensure = (coreKey, time) => { + if (!byCore.has(coreKey)) byCore.set(coreKey, new Map()); + const byTime = byCore.get(coreKey); + if (!byTime.has(time)) byTime.set(time, { inVals: [], outVals: [] }); + return byTime.get(time); + }; + for (const e of events) { + if (e.msg !== "DataFlow") continue; + const time = Math.round(Number(e.Time)); + if (!Number.isFinite(time)) continue; + const value = formatDataAsDecimal(e.Data); + if (e.Behavior === "FeedIn" || e.Behavior === "Recv") { + const dst = parseEndpoint(e.Behavior === "FeedIn" ? e.To : e.Dst); + if (dst?.kind !== "tilePort") continue; + const cell = ensure(tileKey(dst.x, dst.y), time); + if (value != null) cell.inVals.push(value); + continue; + } + if (e.Behavior === "Send" || e.Behavior === "Collect") { + const src = parseEndpoint(e.Behavior === "Collect" ? e.From : e.Src); + if (src?.kind !== "tilePort") continue; + const cell = ensure(tileKey(src.x, src.y), time); + if (value != null) cell.outVals.push(value); + } + } + return byCore; +} + +function refreshCoreFocusControl(columns) { + if (!controls.timingCoreFocus) return; + const options = [ + { value: "", label: "All cores" }, + ...columns.map((c) => ({ value: c.coreKey, label: `(${c.x},${c.y})` })), + ]; + controls.timingCoreFocus.innerHTML = options + .map((opt) => ``) + .join(""); + const hasFocused = columns.some((c) => c.coreKey === state.timingFocusedCoreKey); + if (!hasFocused) state.timingFocusedCoreKey = null; + controls.timingCoreFocus.value = state.timingFocusedCoreKey || ""; +} + +function refreshIoWaveCoreControl(columns) { + if (!controls.timingIoWaveCore || !controls.timingIoWaveAll) return; + const validKeys = new Set(columns.map((c) => c.coreKey)); + const expanded = new Set( + [...(state.timingIoWaveExpandedCoreKeys || [])].filter((key) => validKeys.has(key)), + ); + state.timingIoWaveExpandedCoreKeys = expanded; + + const options = columns.map((c) => ({ value: c.coreKey, label: `(${c.x},${c.y})` })); + controls.timingIoWaveCore.innerHTML = options + .map((opt) => ``) + .join(""); + const shouldSelectAll = Boolean(state.timingIoWaveExpandAll && columns.length > 0); + if (state.timingIoWaveExpandAll && columns.length === 0) { + state.timingIoWaveExpandAll = false; + } + const selectedKeys = shouldSelectAll + ? new Set(columns.map((c) => c.coreKey)) + : expanded; + for (const opt of controls.timingIoWaveCore.options) { + opt.selected = selectedKeys.has(opt.value); + } + controls.timingIoWaveAll.checked = shouldSelectAll; +} + +function renderTimingDrilldown(view, heatmap, phaseExplain, compensation) { + if (!controls.timingDrilldown) return; + if (!state.timingSelectedCell || !heatmap.cells.has(state.timingSelectedCell)) { + controls.timingDrilldown.innerHTML = + "
Click a timeline mark to inspect operation-level details.
"; + return; + } + const selected = heatmap.cells.get(state.timingSelectedCell); + const { slot } = splitCellKey(state.timingSelectedCell); + const items = view.cellMap.get(state.timingSelectedCell) || []; + const corePhase = phaseExplain.coreMap.get(selected.coreKey); + const coreComp = compensation.coreOffsets.get(selected.coreKey) || null; + const compOffset = getModelOffset(coreComp, state.timingCompModel); + const corePhaseText = corePhase?.phaseOffset == null ? "N/A" : formatDelta(corePhase.phaseOffset); + const coreCompText = compOffset == null ? "N/A" : formatDelta(compOffset); + const confPct = `${Math.round((corePhase?.phaseConfidence || 0) * 100)}%`; + const summary = [ + `core=(${selected.x},${selected.y})`, + `edge=${corePhase?.boundaryLabel || "N/A"}`, + `slot=s${Number.isFinite(slot) ? slot : "N/A"}`, + `ops=${selected.opCount}`, + `anomaly=${selected.anomalyCount}`, + `strictPhase=${corePhaseText}`, + `comp(${state.timingCompModel})=${coreCompText}`, + `conf=${confPct}`, + ].join(" | "); + + if (items.length === 0) { + controls.timingDrilldown.innerHTML = + `
${escapeHtml(summary)}
No expected operations in this cell.
`; + return; + } + + let html = `
${escapeHtml(summary)}
`; + html += "
"; + for (const item of items) { + const rowCls = [ + "timing-drill-row", + item.status, + item.firstDivergence ? "first-divergence" : "", + item.propagated ? "propagated" : "", + ].filter(Boolean).join(" "); + const opLabel = `#${item.id} ${item.opcode || "N/A"}`; + const deltaComp = computeDeltaRebased(item.delta, compOffset, view.ii); + const statusComp = statusFromDelta(deltaComp, item.status === "missing"); + const compLabel = state.timingCompModel === "hybrid" ? "hybrid" : `comp(${state.timingCompModel})`; + const allSamples = Array.isArray(item.allSamples) ? item.allSamples : []; + const sourceCounts = new Map(); + for (const s of allSamples) { + const src = String(s?.source || "Unknown"); + sourceCounts.set(src, (sourceCounts.get(src) || 0) + 1); + } + const sourceSummary = sourceCounts.size > 0 + ? [...sourceCounts.entries()].map(([k, v]) => `${k}*${v}`).join(",") + : "N/A"; + const sampleRange = allSamples.length > 0 + ? `${allSamples[0].time}..${allSamples[allSamples.length - 1].time}` + : "N/A"; + const samplePreview = allSamples.length > 0 + ? allSamples.slice(0, 4).map((s, idx) => `${idx + 1}:${s.time}:${s.source || "Unknown"}`).join(",") + : "N/A"; + const fields = [ + `statusComp=${statusComp}`, + `deltaComp(${compLabel})=${formatDelta(deltaComp)}`, + `exp=s${item.expectedSlot}`, + `act=${item.actualSlot == null ? "N/A" : `s${item.actualSlot}`}`, + `statusStrict=${item.status} (reference)`, + `deltaStrict=${formatDelta(item.delta)} (reference)`, + `deltaPhaseRebased=${formatDelta(computeDeltaRebased(item.delta, corePhase?.phaseOffset, view.ii))}`, + `time=${item.firstTime == null ? "N/A" : item.firstTime}`, + `samples=${item.sampleCount}`, + `sourceSummary=${sourceSummary}`, + `sampleRange=${sampleRange}`, + `samplePreview=${samplePreview}`, + `div=${item.firstDivergence ? "yes" : "no"}`, + ].join(" | "); + html += `
${escapeHtml(opLabel)}${escapeHtml(fields)}
`; + } + html += "
"; + controls.timingDrilldown.innerHTML = html; +} + +function renderFocusedCoreMini(view, timeline) { + if (!controls.timingCoreMini) return; + const focusedKey = state.timingFocusedCoreKey; + if (!focusedKey) { + controls.timingCoreMini.innerHTML = + "
Click a Y-axis core label or use the core selector to focus one core.
"; + return; + } + const core = view.columns.find((c) => c.coreKey === focusedKey); + const lane = timeline.lanes.find((l) => l.coreKey === focusedKey); + if (!core || !lane) { + controls.timingCoreMini.innerHTML = + "
Focused core is not visible under current filters.
"; + return; + } + const sourceCounts = new Map(); + for (const item of core.items) { + const samples = Array.isArray(item.allSamples) ? item.allSamples : []; + for (const s of samples) { + const src = String(s?.source || "Unknown"); + sourceCounts.set(src, (sourceCounts.get(src) || 0) + 1); + } + } + const sourceText = sourceCounts.size > 0 + ? [...sourceCounts.entries()].map(([k, v]) => `${k}*${v}`).join(" | ") + : "N/A"; + const windowStart = Number(state.timingWindowStart || timeline.timeMin); + const windowEnd = windowStart + Number(state.timingWindowSize || 1) - 1; + const compModel = ["distance", "fitted", "hybrid"].includes(state.timingCompModel) + ? state.timingCompModel + : "hybrid"; + const rows = lane.expectedSlots + .filter((slot) => { + const inExp = Number.isFinite(slot.expectedTime) && slot.expectedTime >= windowStart && slot.expectedTime <= windowEnd; + const inAct = Number.isFinite(slot.actualTime) && slot.actualTime >= windowStart && slot.actualTime <= windowEnd; + return inExp || inAct; + }) + .sort((a, b) => { + const ta = Number.isFinite(a.actualTime) ? a.actualTime : a.expectedTime; + const tb = Number.isFinite(b.actualTime) ? b.actualTime : b.expectedTime; + if (ta !== tb) return ta - tb; + if (a.opId !== b.opId) return a.opId - b.opId; + return (a.sampleIndex || 0) - (b.sampleIndex || 0); + }) + .slice(0, 28); + const listHtml = rows.length > 0 + ? rows.map((slot) => { + const occ = `[${slot.sampleIndex}/${slot.occurrenceTotal}]`; + const src = slot.sampleSource || "N/A"; + const strict = slot.statusStrict; + const comp = getCompStatusByModel(slot, compModel); + const line = `#${slot.opId}${occ} ${slot.opcode || "N/A"} expT=${slot.expectedTime ?? "N/A"} actT=${slot.actualTime ?? "N/A"} strict=${strict} comp=${comp} src=${src}`; + return `
${escapeHtml(line)}
`; + }).join("") + : "
No blocks from this core in current window.
"; + + controls.timingCoreMini.innerHTML = [ + `
focused-core=(${core.x},${core.y}) | window=T${windowStart}..T${windowEnd} | sources=${escapeHtml(sourceText)}
`, + `
${listHtml}
`, + ].join(""); +} + +function parseProgramYaml(text) { + if (!window.jsyaml) { + throw new Error("js-yaml is unavailable in current page."); + } + const parsed = window.jsyaml.load(text); + const cfg = parsed?.array_config; + if (!cfg || !Array.isArray(cfg.cores)) { + throw new Error("Program YAML must contain array_config.cores."); + } + + const ii = Math.max(0, Math.round(Number(cfg.compiled_ii || 0))); + const arrayColumns = Math.round(Number(cfg.columns)); + const arrayRows = Math.round(Number(cfg.rows)); + const hasArraySize = Number.isFinite(arrayColumns) && Number.isFinite(arrayRows) && arrayColumns > 0 && arrayRows > 0; + const expectedOps = []; + const coreSet = new Map(); + + for (const core of cfg.cores) { + const x = Number(core.column); + const y = Number(core.row); + if (!Number.isFinite(x) || !Number.isFinite(y)) continue; + const coreKey = tileKey(x, y); + if (!coreSet.has(coreKey)) coreSet.set(coreKey, { coreKey, x, y }); + + const entries = Array.isArray(core.entries) ? core.entries : []; + for (const entry of entries) { + const groups = Array.isArray(entry.instructions) ? entry.instructions : []; + for (const ig of groups) { + const fallbackSlot = normalizeSlot(ig.index_per_ii || 0, ii); + const ops = Array.isArray(ig.operations) ? ig.operations : []; + for (const op of ops) { + const id = Number(op.id); + if (!Number.isFinite(id)) continue; + const rawTimeStep = Number(op.time_step); + const hasTimeStep = Number.isFinite(rawTimeStep); + const expectedSlot = normalizeSlot(hasTimeStep ? rawTimeStep : fallbackSlot, ii); + expectedOps.push({ + coreKey, + x, + y, + id: Math.round(id), + opcode: String(op.opcode || ""), + expectedSlot, + rawTimeStep: hasTimeStep ? Math.round(rawTimeStep) : null, + }); + } + } + } + } + + const columns = [...coreSet.values()].sort(sortCore); + const maxSlot = expectedOps.reduce((acc, op) => Math.max(acc, op.expectedSlot), 0); + const slots = ii > 0 + ? Array.from({ length: ii }, (_v, idx) => idx) + : Array.from({ length: maxSlot + 1 }, (_v, idx) => idx); + + return { + ii, + expectedOps, + columns, + slots, + arrayColumns: hasArraySize ? arrayColumns : null, + arrayRows: hasArraySize ? arrayRows : null, + }; +} + +function buildActualByCoreAndId(events, ii) { + const actualByCore = new Map(); + for (const e of events) { + const isInst = e.msg === "Inst"; + const isMemoryDirect = e.msg === "Memory" + && (String(e.Behavior || "") === "LoadDirect" || String(e.Behavior || "") === "StoreDirect"); + if (!isInst && !isMemoryDirect) continue; + if (!Number.isFinite(Number(e.Time)) + || !Number.isFinite(Number(e.ID)) + || !Number.isFinite(Number(e.X)) + || !Number.isFinite(Number(e.Y))) { + continue; + } + const coreKey = tileKey(Number(e.X), Number(e.Y)); + if (!actualByCore.has(coreKey)) actualByCore.set(coreKey, new Map()); + const byId = actualByCore.get(coreKey); + const id = Math.round(Number(e.ID)); + if (!byId.has(id)) byId.set(id, []); + byId.get(id).push({ + time: Math.round(Number(e.Time)), + slot: normalizeSlot(e.Time, ii), + pred: e.Pred, + source: isInst ? "Inst" : String(e.Behavior || "MemoryDirect"), + }); + } + for (const byId of actualByCore.values()) { + for (const samples of byId.values()) { + samples.sort((a, b) => a.time - b.time); + } + } + return actualByCore; +} + +function buildStrictTimingView(programSpec, events) { + const actualByCoreAndId = buildActualByCoreAndId(events, programSpec.ii); + const cellMap = new Map(); + const byCore = new Map(); + + for (const op of programSpec.expectedOps) { + const actuals = actualByCoreAndId.get(op.coreKey)?.get(op.id) || []; + const first = actuals.length > 0 ? actuals[0] : null; + const delta = first ? signedDelta(first.slot, op.expectedSlot, programSpec.ii) : null; + const status = !first ? "missing" : (delta === 0 ? "on-time" : (delta < 0 ? "early" : "late")); + const allSamples = actuals.map((sample, sampleIdx) => { + const sampleDelta = signedDelta(sample.slot, op.expectedSlot, programSpec.ii); + const sampleStatus = sampleDelta === 0 ? "on-time" : (sampleDelta < 0 ? "early" : "late"); + return { + ...sample, + sampleIdx, + delta: sampleDelta, + status: sampleStatus, + }; + }); + const compareItem = { + ...op, + actualSlot: first ? first.slot : null, + firstTime: first ? first.time : null, + sampleCount: actuals.length, + delta, + status, + allSamples, + firstDivergence: false, + propagated: false, + }; + + if (!byCore.has(op.coreKey)) byCore.set(op.coreKey, []); + byCore.get(op.coreKey).push(compareItem); + + const cellKey = `${op.coreKey}|${op.expectedSlot}`; + if (!cellMap.has(cellKey)) cellMap.set(cellKey, []); + cellMap.get(cellKey).push(compareItem); + } + + const columns = programSpec.columns.map((c) => { + const items = byCore.get(c.coreKey) || []; + items.sort((a, b) => { + const ta = Number.isFinite(a.firstTime) ? a.firstTime : Number.POSITIVE_INFINITY; + const tb = Number.isFinite(b.firstTime) ? b.firstTime : Number.POSITIVE_INFINITY; + if (ta !== tb) return ta - tb; + if (a.expectedSlot !== b.expectedSlot) return a.expectedSlot - b.expectedSlot; + return a.id - b.id; + }); + + let hasDivergence = false; + let lastDelta = null; + for (const item of items) { + if (item.status === "on-time") continue; + if (item.status === "missing") { + if (!hasDivergence) { + item.firstDivergence = true; + hasDivergence = true; + } else { + item.propagated = true; + } + continue; + } + if (!hasDivergence || item.delta !== lastDelta) { + item.firstDivergence = true; + hasDivergence = true; + } else { + item.propagated = true; + } + lastDelta = item.delta; + } + + const deltaCounts = new Map(); + const statusCounts = { onTime: 0, early: 0, late: 0, missing: 0 }; + for (const item of items) { + if (item.status === "on-time") statusCounts.onTime += 1; + if (item.status === "early") statusCounts.early += 1; + if (item.status === "late") statusCounts.late += 1; + if (item.status === "missing") statusCounts.missing += 1; + if (item.status === "early" || item.status === "late") { + const k = String(item.delta); + deltaCounts.set(k, (deltaCounts.get(k) || 0) + 1); + } + } + let modeDelta = 0; + let modeCount = 0; + for (const [k, v] of deltaCounts.entries()) { + if (v > modeCount) { + modeCount = v; + modeDelta = Number(k); + } + } + + return { + ...c, + items, + modeDelta, + modeCount, + statusCounts, + earlyLateCount: statusCounts.early + statusCounts.late, + phaseOffset: modeCount > 0 ? modeDelta : null, + phaseConfidence: (statusCounts.early + statusCounts.late) > 0 + ? modeCount / (statusCounts.early + statusCounts.late) + : 0, + }; + }); + + for (const items of cellMap.values()) { + items.sort((a, b) => a.id - b.id); + } + + return { + ii: programSpec.ii, + slots: programSpec.slots, + columns, + cellMap, + }; +} + +function renderTimingView() { + if (!controls.timingGrid || !controls.timingSummary) return; + if (!state.programSpec) { + controls.timingSummary.textContent = "Load program YAML to enable strict timing comparison."; + controls.timingGrid.innerHTML = ""; + if (controls.timingCoreFocus) controls.timingCoreFocus.innerHTML = ""; + if (controls.timingIoWaveCore) controls.timingIoWaveCore.innerHTML = ""; + if (controls.timingIoWaveAll) controls.timingIoWaveAll.checked = false; + if (controls.timingDrilldown) { + controls.timingDrilldown.innerHTML = + "
Load YAML and trace, then click a timeline mark for details.
"; + } + if (controls.timingCoreMini) { + controls.timingCoreMini.innerHTML = + "
Focus one core to inspect local trace details.
"; + } + return; + } + if (state.events.length === 0) { + controls.timingSummary.textContent = "Load trace log to populate timing comparison."; + controls.timingGrid.innerHTML = ""; + if (controls.timingCoreFocus) controls.timingCoreFocus.innerHTML = ""; + if (controls.timingIoWaveCore) controls.timingIoWaveCore.innerHTML = ""; + if (controls.timingIoWaveAll) controls.timingIoWaveAll.checked = false; + if (controls.timingDrilldown) { + controls.timingDrilldown.innerHTML = + "
Load YAML and trace, then click a timeline mark for details.
"; + } + if (controls.timingCoreMini) { + controls.timingCoreMini.innerHTML = + "
Focus one core to inspect local trace details.
"; + } + return; + } + + const view = buildStrictTimingView(state.programSpec, state.events); + state.timingRows = view.columns; + state.timingColumns = view.slots; + state.timingReady = true; + refreshCoreFocusControl(view.columns); + refreshIoWaveCoreControl(view.columns); + const heatmap = buildTimingHeatmap(view); + const phaseExplain = buildPhaseExplain(view); + const compensation = buildCompensationModels(view, phaseExplain, state.events); + + const totals = { onTime: 0, early: 0, late: 0, missing: 0 }; + for (const c of view.columns) { + totals.onTime += c.statusCounts.onTime; + totals.early += c.statusCounts.early; + totals.late += c.statusCounts.late; + totals.missing += c.statusCounts.missing; + } + const filterText = state.timingAnomalyOnly ? "filter=anomaly-only" : "filter=all"; + const boundaryText = state.timingBoundaryOnly ? "scope=boundary-only" : "scope=all-cores"; + const focusedCoreText = state.timingFocusedCoreKey ? `focus=${state.timingFocusedCoreKey}` : "focus=all-cores"; + const phaseText = state.showPhaseExplain + ? `phase(boundary=${formatDelta(phaseExplain.boundaryPhase)} inner=${formatDelta(phaseExplain.innerPhase)} gap=${formatDelta(phaseExplain.phaseGap)})` + : "phase(hidden)"; + const compModel = ["distance", "fitted", "hybrid"].includes(state.timingCompModel) + ? state.timingCompModel + : "hybrid"; + const modelSummary = compensation.models[compModel]; + const compTotals = { onTime: 0, early: 0, late: 0, missing: 0 }; + for (const c of view.columns) { + const compMeta = compensation.coreOffsets.get(c.coreKey) || null; + const compOffset = getModelOffset(compMeta, compModel); + for (const item of c.items) { + const deltaComp = computeDeltaRebased(item.delta, compOffset, view.ii); + const statusComp = statusFromDelta(deltaComp, item.status === "missing"); + if (statusComp === "on-time") compTotals.onTime += 1; + if (statusComp === "early") compTotals.early += 1; + if (statusComp === "late") compTotals.late += 1; + if (statusComp === "missing") compTotals.missing += 1; + } + } + controls.timingSummary.textContent = + `strict baseline | ii=${view.ii || "N/A"} | on-time=${totals.onTime} early=${totals.early} late=${totals.late} missing=${totals.missing} | comp(${compModel}) on-time=${compTotals.onTime} early=${compTotals.early} late=${compTotals.late} missing=${compTotals.missing} gap=${formatDelta(modelSummary?.gap)} | view=${state.timingBaselineView} | ${filterText} | ${boundaryText} | ${focusedCoreText} | ingress=${compensation.ingressSides.join("+")} | ${phaseText}`; + + let visibleColumns = view.columns; + if (state.timingBoundaryOnly) { + visibleColumns = visibleColumns.filter((c) => phaseExplain.coreMap.get(c.coreKey)?.isBoundary); + } + if (state.timingFocusedCoreKey) { + visibleColumns = visibleColumns.filter((c) => c.coreKey === state.timingFocusedCoreKey); + } + const visibleCoreSet = new Set(visibleColumns.map((c) => c.coreKey)); + + if (state.timingSelectedCell && !heatmap.cells.has(state.timingSelectedCell)) { + state.timingSelectedCell = null; + } + if (state.timingSelectedCell) { + const selectedCoreKey = splitCellKey(state.timingSelectedCell).coreKey; + if (!visibleCoreSet.has(selectedCoreKey)) { + state.timingSelectedCell = null; + } + } + if (!state.timingSelectedCell) { + for (const c of visibleColumns) { + for (const slot of view.slots) { + const cell = heatmap.cells.get(`${c.coreKey}|${slot}`); + if (cell && (!state.timingAnomalyOnly || cell.hasAnomaly)) { + state.timingSelectedCell = cell.cellKey; + break; + } + } + if (state.timingSelectedCell) break; + } + } + + const timeline = buildTimelineLanes(view, visibleColumns, phaseExplain, compensation); + const compModelForMismatch = ["distance", "fitted", "hybrid"].includes(state.timingCompModel) + ? state.timingCompModel + : "hybrid"; + let firstHybridMismatchTime = null; + for (const lane of timeline.lanes) { + for (const slot of lane.expectedSlots) { + if (getCompStatusByModel(slot, compModelForMismatch) !== "on-time") { + const t = Number.isFinite(slot.actualTime) ? slot.actualTime : slot.expectedTime; + if (Number.isFinite(t) && (firstHybridMismatchTime == null || t < firstHybridMismatchTime)) { + firstHybridMismatchTime = t; + } + } + } + } + state.firstHybridMismatchTime = firstHybridMismatchTime; + + renderTimelineSvg(view, timeline); + renderTimingDrilldown(view, heatmap, phaseExplain, compensation); + renderFocusedCoreMini(view, timeline); } function summarizeEvent(e) { @@ -193,13 +2365,57 @@ function summarizeEvent(e) { if (e.msg === "Memory") { return `Memory ${e.Behavior} tile=(${e.X},${e.Y}) value=${e.Value} addr=${e.Addr}`; } + if (e.msg === "Backpressure") { + return `Backpressure tile=(${e.X},${e.Y}) dir=${e.DstDir ?? "N/A"} reason=${e.Reason ?? "N/A"} op=${e.OpCode ?? "N/A"} id=${e.ID ?? "N/A"}`; + } return JSON.stringify(e); } +function applyMeshZoomTransform(transform) { + meshZoomTransform = transform || d3.zoomIdentity; + if (sceneRoot) sceneRoot.attr("transform", meshZoomTransform.toString()); +} + +function bindMeshZoom() { + if (!meshZoomBehavior) { + meshZoomBehavior = d3.zoom() + .scaleExtent([0.4, 8]) + .on("zoom", (event) => { + applyMeshZoomTransform(event.transform); + }); + } + meshZoomBehavior + .extent([[0, 0], [layout.width, layout.height]]) + .translateExtent([ + [-layout.width * 1.5, -layout.height * 1.5], + [layout.width * 2.5, layout.height * 2.5], + ]); + svg.call(meshZoomBehavior); + svg.call(meshZoomBehavior.transform, meshZoomTransform); +} + +function renderMeshLegend() { + if (!controls.meshLegend) return; + const legendItems = [ + ["Send", colors.Send], + ["Recv", colors.Recv], + ["FeedIn", colors.FeedIn], + ["Collect", colors.Collect], + ["Inst", colors.Inst], + ["Memory", colors.Memory], + ]; + controls.meshLegend.innerHTML = legendItems.map( + ([name, color]) => + `${name}`, + ).join(""); +} + function drawStaticScene() { svg.selectAll("*").remove(); - staticLayer = svg.append("g"); - dynamicLayer = svg.append("g"); + sceneRoot = svg.append("g").attr("class", "mesh-scene-root"); + applyMeshZoomTransform(meshZoomTransform); + staticLayer = sceneRoot.append("g"); + dynamicLayer = sceneRoot.append("g"); const bg = staticLayer.append("rect"); bg @@ -230,6 +2446,19 @@ function drawStaticScene() { .attr("height", layout.tileSize) .attr("rx", 10); + tileGroup + .selectAll(".tile-report-heat") + .data(tiles) + .join("rect") + .attr("class", (d) => `tile-report-heat tile-report-heat-${d.x}-${d.y}`) + .attr("x", (d) => tileRect(d.x, d.y).x) + .attr("y", (d) => tileRect(d.x, d.y).y) + .attr("width", layout.tileSize) + .attr("height", layout.tileSize) + .attr("rx", 10) + .attr("opacity", 0) + .style("display", "none"); + tileGroup .selectAll("text") .data(tiles) @@ -269,35 +2498,12 @@ function drawStaticScene() { .attr("x", (d) => endpointPoint({ kind: "driver", side: d.side, idx: d.idx }).x + 12) .attr("y", (d) => endpointPoint({ kind: "driver", side: d.side, idx: d.idx }).y + 4) .text((d) => `${d.side[0]}${d.idx}`); - - const legend = staticLayer.append("g").attr("transform", "translate(28, 34)"); - const legendItems = [ - ["Send", colors.Send], - ["Recv", colors.Recv], - ["FeedIn", colors.FeedIn], - ["Collect", colors.Collect], - ["Inst", colors.Inst], - ["Memory", colors.Memory], - ]; - legend - .selectAll("circle") - .data(legendItems) - .join("circle") - .attr("cx", (_d, i) => i * 112) - .attr("cy", 0) - .attr("r", 5) - .attr("fill", (d) => d[1]); - legend - .selectAll("text") - .data(legendItems) - .join("text") - .attr("class", "legend-text") - .attr("x", (_d, i) => i * 112 + 9) - .attr("y", 4) - .text((d) => d[0]); + applyReportHeatOverlay(); + renderMeshLegend(); + bindMeshZoom(); } -function drawLink(type, srcPoint, dstPoint) { +function drawLink(type, srcPoint, dstPoint, payload = null) { const path = d3.path(); path.moveTo(srcPoint.x, srcPoint.y); const dx = dstPoint.x - srcPoint.x; @@ -312,13 +2518,52 @@ function drawLink(type, srcPoint, dstPoint) { dstPoint.y, ); - dynamicLayer + const link = dynamicLayer .append("path") .attr("class", "event-link") .attr("d", path.toString()) .attr("stroke", colors[type] || "#555") .attr("stroke-opacity", 0.78); + const dataText = payload?.dataText == null ? "" : String(payload.dataText); + if (payload?.drawDataLabel && dataText) { + const shortData = shortText(dataText, 12); + let anchorX = srcPoint.x + dx * 0.58; + let anchorY = srcPoint.y + dy * 0.58; + try { + const node = link.node(); + if (node) { + const total = node.getTotalLength(); + if (Number.isFinite(total) && total > 0) { + const p = node.getPointAtLength(total * 0.58); + anchorX = p.x; + anchorY = p.y; + } + } + } catch (_) { + // Fall back to linear interpolation point when path metrics unavailable. + } + const tag = dynamicLayer.append("g") + .attr("class", "flow-data-tag") + .attr("transform", `translate(${anchorX},${anchorY})`); + const text = tag.append("text") + .attr("class", "flow-data-text") + .attr("text-anchor", "middle") + .attr("dominant-baseline", "middle") + .text(shortData); + const box = text.node()?.getBBox(); + if (box) { + tag.insert("rect", "text") + .attr("class", "flow-data-bg") + .attr("x", box.x - 3) + .attr("y", box.y - 1) + .attr("width", box.width + 6) + .attr("height", box.height + 2) + .attr("rx", 4); + } + tag.append("title").text(`data=${dataText}`); + } + const pulse = dynamicLayer .append("circle") .attr("class", "pulse") @@ -346,61 +2591,165 @@ function applyTileActivity(activeTiles) { staticLayer.selectAll(".tile-label").style("display", state.showLabels ? null : "none"); } +function shortText(value, maxLen = 16) { + const s = String(value ?? "").trim(); + if (!s) return ""; + return s.length <= maxLen ? s : `${s.slice(0, maxLen - 1)}~`; +} + +function summarizeTokens(tokens, prefix, maxItems = 3) { + if (!Array.isArray(tokens) || tokens.length === 0) return null; + const counts = new Map(); + for (const token of tokens) { + const key = String(token || "N/A"); + counts.set(key, (counts.get(key) || 0) + 1); + } + const sorted = [...counts.entries()] + .sort((a, b) => { + if (b[1] !== a[1]) return b[1] - a[1]; + return a[0].localeCompare(b[0]); + }); + const picked = sorted.slice(0, maxItems).map(([k, v]) => (v > 1 ? `${k}*${v}` : k)); + const remain = sorted.length - maxItems; + const suffix = remain > 0 ? `,+${remain}` : ""; + return `${prefix}:${picked.join(",")}${suffix}`; +} + +function summarizeData(values, prefix, maxItems = 4) { + if (!Array.isArray(values) || values.length === 0) return null; + const picked = values.slice(0, maxItems).map((v) => shortText(v, 8)); + const suffix = values.length > maxItems ? ",..." : ""; + return `${prefix}:${picked.join(",")}${suffix}`; +} + function drawTileBadges(timeEvents) { - const instCounts = new Map(); - const memCounts = new Map(); + const byTile = new Map(); + const ensure = (key) => { + if (!byTile.has(key)) { + byTile.set(key, { + op: [], + mem: [], + txData: [], + rxData: [], + details: [], + }); + } + return byTile.get(key); + }; for (const e of timeEvents) { - if (e.msg === "Inst" && state.showInst) { - const k = tileKey(e.X, e.Y); - instCounts.set(k, (instCounts.get(k) || 0) + 1); + if (e.msg === "Inst" && state.showInst && Number.isFinite(Number(e.X)) && Number.isFinite(Number(e.Y))) { + const k = tileKey(Number(e.X), Number(e.Y)); + const rec = ensure(k); + const op = shortText(e.OpCode || "Inst", 10); + rec.op.push(op || "Inst"); + rec.details.push(`Inst#${e.ID ?? "?"} ${e.OpCode ?? "N/A"} pred=${e.Pred ?? "N/A"}`); + continue; } - if (e.msg === "Memory" && state.showMemory) { - const k = tileKey(e.X, e.Y); - memCounts.set(k, (memCounts.get(k) || 0) + 1); + + if (e.msg === "Memory" && state.showMemory && Number.isFinite(Number(e.X)) && Number.isFinite(Number(e.Y))) { + const k = tileKey(Number(e.X), Number(e.Y)); + const rec = ensure(k); + const behavior = String(e.Behavior || "Memory"); + const memTag = behavior === "LoadDirect" + ? `LD(${shortText(e.Value, 6)})` + : (behavior === "StoreDirect" + ? `ST(${shortText(e.Value, 6)})` + : shortText(behavior, 12)); + rec.mem.push(memTag); + rec.details.push(`Memory ${behavior} value=${e.Value ?? "N/A"} addr=${e.Addr ?? "N/A"}`); + continue; + } + + if (e.msg === "DataFlow" && state.showDataFlow) { + const dataValue = e.Data; + if (e.Behavior === "Send") { + const src = parseEndpoint(e.Src); + if (src?.kind === "tilePort") { + const rec = ensure(tileKey(src.x, src.y)); + rec.txData.push(dataValue); + rec.details.push(`TX ${dataValue} ${e.Src} -> ${e.Dst}`); + } + } else if (e.Behavior === "Recv") { + const dst = parseEndpoint(e.Dst); + if (dst?.kind === "tilePort") { + const rec = ensure(tileKey(dst.x, dst.y)); + rec.rxData.push(dataValue); + rec.details.push(`RX ${dataValue} ${e.Src} -> ${e.Dst}`); + } + } else if (e.Behavior === "FeedIn") { + const dst = parseEndpoint(e.To); + if (dst?.kind === "tilePort") { + const rec = ensure(tileKey(dst.x, dst.y)); + rec.rxData.push(dataValue); + rec.details.push(`FeedIn ${dataValue} ${e.From} -> ${e.To}`); + } + } else if (e.Behavior === "Collect") { + const src = parseEndpoint(e.From); + if (src?.kind === "tilePort") { + const rec = ensure(tileKey(src.x, src.y)); + rec.txData.push(dataValue); + rec.details.push(`Collect ${dataValue} ${e.From} -> ${e.To || e.Dst || "Driver"}`); + } + } } } - for (const [k, count] of instCounts.entries()) { + for (const [k, rec] of byTile.entries()) { const [x, y] = k.split(",").map(Number); const r = tileRect(x, y); - dynamicLayer - .append("circle") - .attr("class", "inst-badge") - .attr("cx", r.x + 16) - .attr("cy", r.y + 16) - .attr("r", 10) - .attr("fill", colors.Inst) - .attr("opacity", 0.9); - dynamicLayer - .append("text") - .attr("x", r.x + 12) - .attr("y", r.y + 20) - .attr("fill", "#fff") - .attr("font-size", 11) - .text(`${count}`); - } - - for (const [k, count] of memCounts.entries()) { - const [x, y] = k.split(",").map(Number); - const r = tileRect(x, y); - dynamicLayer - .append("rect") - .attr("class", "memory-badge") - .attr("x", r.x + r.w - 23) - .attr("y", r.y + r.h - 23) - .attr("width", 16) - .attr("height", 16) - .attr("rx", 3) - .attr("fill", colors.Memory) - .attr("opacity", 0.9); - dynamicLayer - .append("text") - .attr("x", r.x + r.w - 20) - .attr("y", r.y + r.h - 11) - .attr("fill", "#fff") - .attr("font-size", 11) - .text(`${count}`); + const lineHeight = clamp(Math.round(layout.tileSize * 0.12), 9, 13); + const fontSize = clamp(Math.round(layout.tileSize * 0.1), 7, 11); + const innerWidth = Math.max(20, r.w - 8); + const approxCharWidth = fontSize * 0.6; + const maxCharsPerLine = Math.max(4, Math.floor(innerWidth / approxCharWidth)); + const textTop = r.y + 24; + const maxTextHeight = Math.max(8, r.h - 30); + const maxLines = Math.max(1, Math.floor(maxTextHeight / lineHeight)); + + const lines = []; + const lineOps = [summarizeTokens(rec.op, "OP", 2), summarizeTokens(rec.mem, "MEM", 1)] + .filter(Boolean) + .join(" | "); + const lineFlow = [summarizeData(rec.rxData, "RX", 2), summarizeData(rec.txData, "TX", 2)] + .filter(Boolean) + .join(" | "); + // Prioritize flow values so data is still visible when space is tight. + if (lineFlow) lines.push(lineFlow); + if (lineOps) lines.push(lineOps); + if (lines.length === 0) continue; + + const shown = lines.slice(0, maxLines).map((line) => shortText(line, maxCharsPerLine)); + if (lines.length > maxLines) { + shown[maxLines - 1] = `${shortText(shown[maxLines - 1], Math.max(4, maxCharsPerLine - 3))}...`; + } + const bgHeight = shown.length * lineHeight + 8; + const g = dynamicLayer.append("g") + .attr("class", "tile-overlay") + .attr("transform", `translate(${r.x + 4},${textTop})`); + g.append("rect") + .attr("class", "tile-overlay-card") + .attr("width", innerWidth) + .attr("height", bgHeight) + .attr("rx", 4); + const text = g.append("text") + .attr("class", "tile-overlay-text") + .style("font-size", `${fontSize}px`) + .attr("x", 4) + .attr("y", lineHeight - 1); + shown.forEach((line, idx) => { + text.append("tspan") + .attr("x", 4) + .attr("dy", idx === 0 ? 0 : lineHeight) + .text(line); + }); + g.append("title").text( + [ + `tile=(${x},${y})`, + ...lines, + ...rec.details.slice(0, 12), + ].join("\n"), + ); } } @@ -421,13 +2770,15 @@ function renderCycleDetails(events, t) { } function renderTime(t) { - state.currentTime = t; - controls.timeLabel.textContent = `T=${t}`; - controls.timeSlider.value = String(t); + const cycle = clamp(normalizeCycleTime(t, state.currentTime), state.minTime, state.maxTime); + state.currentTime = cycle; + controls.timeLabel.textContent = `T=${cycle}`; + controls.timeSlider.value = String(cycle); dynamicLayer.selectAll("*").remove(); - const events = state.byTime.get(t) || []; + const events = state.byTime.get(cycle) || []; const activeTiles = new Set(); + const linkLabelSeen = new Set(); for (const e of events) { if (e.msg === "DataFlow" && state.showDataFlow) { @@ -446,7 +2797,11 @@ function renderTime(t) { const srcPoint = endpointPoint(src); const dstPoint = endpointPoint(dst); if (srcPoint && dstPoint) { - drawLink(type, srcPoint, dstPoint); + const dataText = e.Data == null ? "" : String(e.Data); + const labelKey = `${src?.raw || srcPoint.tile || "?"}|${dst?.raw || dstPoint.tile || "?"}|${dataText}`; + const drawDataLabel = dataText && !linkLabelSeen.has(labelKey); + if (drawDataLabel) linkLabelSeen.add(labelKey); + drawLink(type, srcPoint, dstPoint, { dataText, drawDataLabel }); } else if (srcPoint) { dynamicLayer .append("circle") @@ -474,45 +2829,73 @@ function renderTime(t) { applyTileActivity(activeTiles); drawTileBadges(events); - renderCycleDetails(events, t); + // Keep link arrows above tile overlay cards. + dynamicLayer.selectAll(".event-link").raise(); + // Keep transfer data labels/pulses above tile cards for readability. + dynamicLayer.selectAll(".flow-data-tag").raise(); + dynamicLayer.selectAll(".pulse").raise(); + renderCycleDetails(events, cycle); } function stopPlayback() { if (state.timer) { - clearInterval(state.timer); + clearTimeout(state.timer); state.timer = null; } controls.playBtn.textContent = "Play"; } +function playbackTick() { + if (!state.timer) return; + const next = nextIndexedTime(state.currentTime, +1); + if (next <= state.currentTime) { + stopPlayback(); + return; + } + renderTime(next); + state.timer = setTimeout(playbackTick, state.speedMs); +} + function playOrPause() { if (state.timer) { stopPlayback(); return; } + if (state.currentTime >= state.maxTime) renderTime(state.maxTime); controls.playBtn.textContent = "Pause"; - state.timer = setInterval(() => { - if (state.currentTime >= state.maxTime) { - stopPlayback(); - return; - } - renderTime(state.currentTime + 1); - }, state.speedMs); + state.timer = setTimeout(playbackTick, state.speedMs); } function initControls() { controls.playBtn.addEventListener("click", playOrPause); controls.stepBackBtn.addEventListener("click", () => { + if (state.stepLock) return; + state.stepLock = true; stopPlayback(); - renderTime(Math.max(state.minTime, state.currentTime - 1)); + try { + renderTime(nextIndexedTime(state.currentTime, -1)); + } finally { + state.stepLock = false; + } }); controls.stepFwdBtn.addEventListener("click", () => { + if (state.stepLock) return; + state.stepLock = true; stopPlayback(); - renderTime(Math.min(state.maxTime, state.currentTime + 1)); + try { + renderTime(nextIndexedTime(state.currentTime, +1)); + } finally { + state.stepLock = false; + } }); controls.timeSlider.addEventListener("input", (e) => { + const wasPlaying = Boolean(state.timer); stopPlayback(); - renderTime(Number(e.target.value)); + const nextTime = Number(e.target.value); + renderTime(nextTime); + if (wasPlaying) { + playOrPause(); + } }); controls.speedSelect.addEventListener("change", (e) => { state.speedMs = Number(e.target.value); @@ -537,23 +2920,198 @@ function initControls() { state.showLabels = Boolean(e.target.checked); renderTime(state.currentTime); }); + if (controls.timingAnomalyOnly) { + controls.timingAnomalyOnly.checked = state.timingAnomalyOnly; + controls.timingAnomalyOnly.addEventListener("change", (e) => { + state.timingAnomalyOnly = Boolean(e.target.checked); + renderTimingView(); + }); + } + if (controls.timingShowPhaseExplain) { + controls.timingShowPhaseExplain.checked = state.showPhaseExplain; + controls.timingShowPhaseExplain.addEventListener("change", (e) => { + state.showPhaseExplain = Boolean(e.target.checked); + renderTimingView(); + }); + } + if (controls.timingBoundaryOnly) { + controls.timingBoundaryOnly.checked = state.timingBoundaryOnly; + controls.timingBoundaryOnly.addEventListener("change", (e) => { + state.timingBoundaryOnly = Boolean(e.target.checked); + state.timingSelectedCell = null; + renderTimingView(); + }); + } + if (controls.timingCoreFocus) { + controls.timingCoreFocus.addEventListener("change", (e) => { + const value = String(e.target.value || ""); + state.timingFocusedCoreKey = value || null; + state.timingSelectedCell = null; + renderTimingView(); + }); + } + if (controls.timingIoWaveAll) { + controls.timingIoWaveAll.addEventListener("change", (e) => { + const checked = Boolean(e.target.checked); + state.timingIoWaveExpandAll = checked; + if (checked) { + state.timingIoWaveExpandedCoreKeys = new Set((state.timingRows || []).map((c) => c.coreKey)); + } + renderTimingView(); + }); + } + if (controls.timingIoWaveCore) { + controls.timingIoWaveCore.addEventListener("change", (e) => { + const selectedKeys = new Set( + [...e.target.selectedOptions] + .map((opt) => String(opt.value || "")) + .filter(Boolean), + ); + state.timingIoWaveExpandedCoreKeys = selectedKeys; + const total = (state.timingRows || []).length; + state.timingIoWaveExpandAll = total > 0 && selectedKeys.size >= total; + renderTimingView(); + }); + } + if (controls.timingBaselineView) { + controls.timingBaselineView.value = state.timingBaselineView; + controls.timingBaselineView.addEventListener("change", (e) => { + state.timingBaselineView = String(e.target.value || "strict"); + renderTimingView(); + }); + } + if (controls.timingCompModel) { + controls.timingCompModel.value = state.timingCompModel; + controls.timingCompModel.addEventListener("change", (e) => { + state.timingCompModel = String(e.target.value || "hybrid"); + renderTimingView(); + }); + } + if (controls.timingExportPng) { + controls.timingExportPng.addEventListener("click", exportTimelinePng); + } + if (controls.timingJumpFirstMismatch) { + controls.timingJumpFirstMismatch.addEventListener("click", () => { + if (state.firstHybridMismatchTime == null || !Number.isFinite(state.firstHybridMismatchTime)) return; + const half = Math.floor((Number(state.timingWindowSize) || 60) / 2); + state.timingWindowStart = Math.max(0, state.firstHybridMismatchTime - half); + renderTimingView(); + }); + } + if (controls.timingWindowStart) { + controls.timingWindowStart.addEventListener("input", (e) => { + state.timingWindowStart = Number(e.target.value); + renderTimingView(); + }); + } + if (controls.timingWindowSize) { + controls.timingWindowSize.addEventListener("input", (e) => { + state.timingWindowSize = Number(e.target.value); + renderTimingView(); + }); + } + if (controls.timingZoomY) { + controls.timingZoomY.addEventListener("input", (e) => { + state.timingZoomY = clamp(Number(e.target.value) / 100, 0.6, 4); + renderTimingView(); + }); + } + if (controls.timingResetZoom) { + controls.timingResetZoom.addEventListener("click", () => { + state.timingZoomX = 1; + state.timingZoomY = 1; + const fullMin = state.timingViewport?.fullMin ?? state.minTime; + const fullMax = state.timingViewport?.fullMax ?? state.maxTime; + const fullSpan = Math.max(1, fullMax - fullMin + 1); + state.timingWindowSize = Math.min(120, fullSpan); + state.timingWindowStart = fullMin; + renderTimingView(); + }); + } + if (controls.timingGrid) { + controls.timingGrid.addEventListener("wheel", handleTimelineCtrlWheelZoom, { passive: false }); + controls.timingGrid.addEventListener("click", (e) => { + const label = e.target.closest("[data-core-key]"); + if (label) { + if (timingCoreLabelClickTimer) clearTimeout(timingCoreLabelClickTimer); + const key = label.getAttribute("data-core-key"); + timingCoreLabelClickTimer = setTimeout(() => { + timingCoreLabelClickTimer = null; + state.timingFocusedCoreKey = state.timingFocusedCoreKey === key ? null : key; + state.timingSelectedCell = null; + renderTimingView(); + }, 220); + return; + } + const btn = e.target.closest("[data-timing-cell]"); + if (!btn) return; + state.timingSelectedCell = btn.getAttribute("data-timing-cell"); + renderTimingView(); + }); + controls.timingGrid.addEventListener("dblclick", (e) => { + const label = e.target.closest("[data-core-key]"); + if (!label) return; + if (timingCoreLabelClickTimer) { + clearTimeout(timingCoreLabelClickTimer); + timingCoreLabelClickTimer = null; + } + const key = label.getAttribute("data-core-key"); + if (!key) return; + const expanded = new Set(state.timingIoWaveExpandedCoreKeys || []); + if (expanded.has(key)) { + expanded.delete(key); + } else { + expanded.add(key); + } + state.timingIoWaveExpandedCoreKeys = expanded; + const total = (state.timingRows || []).length; + state.timingIoWaveExpandAll = total > 0 && expanded.size >= total; + renderTimingView(); + }); + } controls.fileInput.addEventListener("change", async (e) => { const file = e.target.files?.[0]; if (!file) return; const text = await file.text(); loadTrace(text); }); + controls.yamlInput.addEventListener("change", async (e) => { + const file = e.target.files?.[0]; + if (!file) return; + const text = await file.text(); + loadProgramYaml(text); + }); + if (controls.reportInput) { + controls.reportInput.addEventListener("change", async (e) => { + const file = e.target.files?.[0]; + if (!file) return; + const text = await file.text(); + loadReport(text); + }); + } } function loadTrace(text) { stopPlayback(); const events = parseJsonLines(text); state.events = events; - const bounds = inferBounds(events); + state.coreIoWaveByTime = buildCoreIoWaveByTime(events); + state.timingSelectedCell = null; + state.timingFocusedCoreKey = null; + state.timingIoWaveExpandAll = false; + state.timingIoWaveExpandedCoreKeys = new Set(); + state.timingWindowStart = 0; + state.timingWindowSize = 120; + state.timingZoomX = 1; + state.timingZoomY = 1; + state.timingViewport = null; + meshZoomTransform = d3.zoomIdentity; + const bounds = resolveMeshBounds(events); state.maxX = bounds.maxX; state.maxY = bounds.maxY; const index = indexByTime(events); state.byTime = index.byTime; + state.timeKeys = index.sortedTimes; state.minTime = index.minTime; state.maxTime = index.maxTime; @@ -561,12 +3119,84 @@ function loadTrace(text) { controls.timeSlider.max = String(state.maxTime); controls.timeSlider.value = String(state.minTime); + applyAdaptiveLayout(); drawStaticScene(); renderTime(state.minTime); + renderReportView(); + renderTimingView(); +} + +function loadProgramYaml(text) { + try { + state.programSpec = parseProgramYaml(text); + state.yamlGridBounds = boundsFromProgramSpec(state.programSpec); + state.timingSelectedCell = null; + state.timingFocusedCoreKey = null; + state.timingIoWaveExpandAll = false; + state.timingIoWaveExpandedCoreKeys = new Set(); + state.timingWindowStart = 0; + state.timingZoomX = 1; + state.timingZoomY = 1; + state.timingViewport = null; + meshZoomTransform = d3.zoomIdentity; + const bounds = state.yamlGridBounds || inferBounds(state.events); + state.maxX = bounds.maxX; + state.maxY = bounds.maxY; + applyAdaptiveLayout(); + drawStaticScene(); + if (state.events.length > 0) { + renderTime(clamp(state.currentTime, state.minTime, state.maxTime)); + } + renderReportView(); + renderTimingView(); + } catch (err) { + state.programSpec = null; + state.yamlGridBounds = null; + state.timingReady = false; + state.timingFocusedCoreKey = null; + state.timingIoWaveExpandAll = false; + state.timingIoWaveExpandedCoreKeys = new Set(); + controls.timingSummary.textContent = `Program YAML parse error: ${err.message}`; + controls.timingGrid.innerHTML = ""; + if (controls.timingCoreFocus) { + controls.timingCoreFocus.innerHTML = ""; + controls.timingCoreFocus.value = ""; + } + if (controls.timingIoWaveCore) { + controls.timingIoWaveCore.innerHTML = ""; + } + if (controls.timingIoWaveAll) { + controls.timingIoWaveAll.checked = false; + } + if (controls.timingDrilldown) { + controls.timingDrilldown.innerHTML = + "
Program YAML parse failed. Fix YAML and reload.
"; + } + if (controls.timingCoreMini) { + controls.timingCoreMini.innerHTML = + "
Focus one core to inspect local trace details.
"; + } + renderReportView(); + } +} + +let resizeTimer = null; + +function handleResize() { + if (state.events.length === 0 && !state.programSpec) return; + if (resizeTimer) clearTimeout(resizeTimer); + resizeTimer = setTimeout(() => { + applyAdaptiveLayout(); + drawStaticScene(); + if (state.events.length > 0) renderTime(state.currentTime); + }, 120); } async function boot() { initControls(); + applyAdaptiveLayout(); + renderReportView(); + window.addEventListener("resize", handleResize); // Default behavior: load ../gemm.json.log when served from repo root. try { @@ -578,6 +3208,15 @@ async function boot() { controls.statsLine.textContent = "Default log not loaded. Use the file picker."; controls.eventDump.textContent = ""; } + + try { + const yamlResp = await fetch("../gemm.yaml"); + if (!yamlResp.ok) throw new Error(`HTTP ${yamlResp.status}`); + const yamlText = await yamlResp.text(); + loadProgramYaml(yamlText); + } catch (_) { + renderTimingView(); + } } boot(); diff --git a/tool/viz/index.html b/tool/viz/index.html index cd8528b..2a8caa9 100644 --- a/tool/viz/index.html +++ b/tool/viz/index.html @@ -3,20 +3,31 @@ - CGRA GEMM Log Viewer + CGRA Log Viewer +
-

CGRA GEMM Log Viewer

+

CGRA Log Viewer

Timeline visualization for JSONL execution traces

- - +
+ + +
+
+ + +
+
+ + +
@@ -45,7 +56,8 @@

CGRA GEMM Log Viewer

- +
+
@@ -53,6 +65,99 @@

Cycle Details


       
+ +
+

Report Overview

+

+
+
+
+ +
+

Strict Timing Offset View

+

+
+ + + + + + + + + + + +
+
+ + + +
+
+ on-time + early + late + missing + propagated +
+
+
+
+ +
+
+
diff --git a/tool/viz/styles.css b/tool/viz/styles.css index 0f2adf1..4bc606d 100644 --- a/tool/viz/styles.css +++ b/tool/viz/styles.css @@ -30,6 +30,7 @@ body { radial-gradient(circle at 10% 90%, #8ecae644, transparent 38%), var(--bg); font-family: "Avenir Next", "Segoe UI", "Helvetica Neue", sans-serif; + scrollbar-gutter: stable both-edges; } .layout { @@ -47,6 +48,7 @@ body { border-radius: 14px; padding: 0.9rem 1rem; box-shadow: var(--shadow); + min-width: 0; } .topbar { @@ -75,10 +77,17 @@ h2 { .file-load { display: flex; align-items: center; - gap: 0.5rem; + gap: 0.8rem; + flex-wrap: wrap; font-size: 0.92rem; } +.file-load-item { + display: flex; + align-items: center; + gap: 0.4rem; +} + .controls .row { display: flex; gap: 0.6rem; @@ -118,8 +127,38 @@ button { padding: 0.2rem; } +.mesh-legend-panel { + display: flex; + flex-wrap: wrap; + gap: 0.45rem 0.8rem; + align-items: center; + margin: 0.1rem 0.15rem 0.5rem; + padding: 0.35rem 0.45rem; + border: 1px solid #d7caaa; + border-radius: 10px; + background: #fbf5e7; +} + +.mesh-legend-item { + display: inline-flex; + align-items: center; + gap: 0.35rem; + font-size: 0.82rem; + color: #544b3f; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; +} + +.mesh-legend-dot { + width: 10px; + height: 10px; + border-radius: 999px; + border: 1px solid rgba(48, 48, 48, 0.35); + display: inline-block; +} + #canvas { width: 100%; + height: auto; min-height: 480px; display: block; } @@ -131,7 +170,7 @@ button { } #eventDump { - max-height: 260px; + height: 260px; overflow: auto; background: #fbf7ea; border: 1px solid var(--line); @@ -140,6 +179,100 @@ button { margin-top: 0.6rem; font-size: 0.82rem; line-height: 1.45; + white-space: pre-wrap; + word-break: break-word; + overflow-wrap: anywhere; +} + +.report-warning { + margin: 0 0 0.45rem; + color: #756f63; + font-size: 0.86rem; +} + +.report-warning.warn { + color: #905b13; +} + +.report-warning.error { + color: #9d1f1f; +} + +.report-summary { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); + gap: 0.45rem; + margin-bottom: 0.55rem; +} + +.report-card { + border: 1px solid #ddcfb2; + border-radius: 8px; + background: #fbf7ea; + padding: 0.42rem 0.5rem; +} + +.report-card-k { + font-size: 0.73rem; + color: #7a6f58; + text-transform: uppercase; + letter-spacing: 0.02em; +} + +.report-card-v { + margin-top: 0.1rem; + font-size: 1rem; + color: #3b352d; + font-weight: 600; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; +} + +.report-hot-tiles { + border: 1px solid #ddcfb2; + border-radius: 10px; + background: #fffaf0; + overflow: auto; + max-height: 280px; +} + +.report-hot-title { + position: sticky; + top: 0; + z-index: 1; + padding: 0.42rem 0.55rem; + border-bottom: 1px solid #ddcfb2; + background: #f4ecd9; + color: #5c5348; + font-size: 0.82rem; + font-weight: 600; +} + +.report-hot-table { + width: 100%; + border-collapse: collapse; +} + +.report-hot-table th, +.report-hot-table td { + border-bottom: 1px solid #eadfc7; + padding: 0.28rem 0.4rem; + text-align: left; + font-size: 0.79rem; + color: #4f483c; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; +} + +.report-hot-table thead th { + position: sticky; + top: 29px; + background: #f8f0de; + z-index: 1; +} + +.report-empty { + padding: 0.55rem; + color: #817662; + font-size: 0.84rem; } .tile { @@ -148,6 +281,11 @@ button { stroke-width: 1.4px; } +.tile-report-heat { + fill: #d62828; + pointer-events: none; +} + .tile.active { fill: var(--tile-active); } @@ -158,6 +296,21 @@ button { pointer-events: none; } +.tile-overlay-card, +.tile-overlay-bg { + fill: rgba(255, 253, 247, 0.9); + stroke: rgba(122, 109, 84, 0.5); + stroke-width: 0.75px; +} + +.tile-overlay-text { + fill: #3a352d; + font-size: 9.5px; + font-weight: 600; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + pointer-events: none; +} + .legend-text, .driver-label { fill: #4d4d4d; @@ -170,6 +323,23 @@ button { stroke-linecap: round; } +.flow-data-tag { + pointer-events: none; +} + +.flow-data-bg { + fill: rgba(255, 251, 240, 0.92); + stroke: rgba(122, 109, 84, 0.48); + stroke-width: 0.7px; +} + +.flow-data-text { + fill: #3b3427; + font-size: 9px; + font-weight: 700; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; +} + .pulse { r: 5; } @@ -180,8 +350,678 @@ button { stroke-width: 0.5px; } +.timing-summary { + margin: 0 0 0.45rem; + color: var(--muted); + font-size: 0.9rem; +} + +.timing-toolbar { + display: flex; + align-items: center; + justify-content: flex-start; + flex-wrap: wrap; + gap: 0.7rem; + margin: 0 0 0.45rem; +} + +.timing-toggle { + display: inline-flex; + align-items: center; + gap: 0.35rem; + font-size: 0.84rem; + color: #5b5b5b; +} + +.timing-select select { + min-width: 130px; + padding: 0.18rem 0.3rem; + border: 1px solid #cbbda2; + border-radius: 6px; + background: #fffdf7; + color: #514a3f; +} + +#timingIoWaveCore[multiple] { + min-width: 140px; + min-height: 92px; +} + +.timing-export-setting input[type="number"] { + width: 86px; + padding: 0.2rem 0.35rem; +} + +.timing-legend { + display: flex; + gap: 0.45rem; + align-items: center; + flex-wrap: wrap; + margin-bottom: 0.55rem; +} + +.legend-chip { + border: 1px solid #bfb8a6; + border-radius: 999px; + padding: 0.12rem 0.5rem; + font-size: 0.77rem; +} + +.legend-chip.on-time { + background: #d9f5df; +} + +.legend-chip.early { + background: #ffd9d9; +} + +.legend-chip.late { + background: #ffc8c8; +} + +.legend-chip.missing { + background: #ececec; +} + +.legend-chip.propagated { + background: #f5e4e4; + opacity: 0.75; +} + +.timing-grid-wrap { + overflow: auto; + border: 1px solid var(--line); + border-radius: 10px; + background: #fbf7ea; +} + +.timing-grid, +.timing-heatmap { + width: max-content; + min-width: 100%; + border-collapse: collapse; +} + +.timing-grid th, +.timing-grid td, +.timing-heatmap th, +.timing-heatmap td { + border-bottom: 1px solid #d8ccb0; + border-right: 1px solid #e4d8be; + vertical-align: top; + padding: 0.35rem 0.4rem; +} + +.timing-grid thead th, +.timing-heatmap thead th { + position: sticky; + top: 0; + background: #f4ecd9; + z-index: 1; + font-size: 0.78rem; + white-space: nowrap; +} + +.core-col { + min-width: 176px; + color: #544f43; + font-size: 0.8rem; + background: #f8f1df; + text-align: left; +} + +.slot-head { + min-width: 84px; + text-align: left; +} + +.timing-grid thead .core-col, +.timing-heatmap thead .core-col { + left: 0; + z-index: 3; +} + +.timing-grid tbody .core-col, +.timing-heatmap tbody .core-col { + position: sticky; + left: 0; + z-index: 2; +} + +.core-col .core-meta { + color: #7a6b55; + font-size: 0.72rem; + margin-top: 0.15rem; + font-weight: 500; +} + +.timing-heat-cell { + padding: 0.2rem 0.25rem; +} + +.timing-heat-btn { + width: 100%; + min-width: 64px; + border-radius: 8px; + border: 1px solid #c8baa0; + padding: 0.2rem 0.26rem; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.08rem; + cursor: pointer; + background: #fff; + transition: transform 120ms ease, box-shadow 120ms ease; +} + +.timing-heat-btn:hover { + transform: translateY(-1px); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06), 0 2px 8px rgba(0, 0, 0, 0.08); +} + +.timing-heat-btn.selected { + box-shadow: 0 0 0 2px rgba(73, 83, 102, 0.35) inset; +} + +.timing-heat-btn.first-divergence { + border-width: 2px; +} + +.timing-heat-btn.boundary-core { + box-shadow: 0 0 0 1px rgba(66, 66, 66, 0.18); +} + +.timing-heat-btn.muted { + opacity: 0.25; +} + +.timing-heat-btn .heat-main { + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 0.76rem; + font-weight: 700; + line-height: 1; +} + +.timing-heat-btn .heat-sub { + font-size: 0.67rem; + color: #5f5f5f; + line-height: 1.1; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; +} + +.timing-heat-btn.status-on-time { + background: rgba(217, 245, 223, var(--heat-alpha, 0.2)); + border-color: #8bbd95; +} + +.timing-heat-btn.status-early { + background: rgba(255, 217, 217, var(--heat-alpha, 0.5)); + border-color: #d88b8b; +} + +.timing-heat-btn.status-late { + background: rgba(255, 200, 200, var(--heat-alpha, 0.56)); + border-color: #d77a7a; +} + +.timing-heat-btn.status-missing { + background: rgba(236, 236, 236, var(--heat-alpha, 0.62)); + border-color: #9a9696; +} + +.timing-drilldown { + margin-top: 0.6rem; + border: 1px solid #dacfb5; + border-radius: 10px; + background: #fffaf0; + max-height: 280px; + overflow: auto; +} + +.timing-drill-head { + position: sticky; + top: 0; + z-index: 1; + background: #f4ecd9; + border-bottom: 1px solid #dacfb5; + color: #5f5548; + font-size: 0.78rem; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + padding: 0.45rem 0.55rem; + white-space: nowrap; + overflow: auto; +} + +.timing-drill-list { + display: flex; + flex-direction: column; +} + +.timing-drill-row { + display: grid; + grid-template-columns: minmax(120px, 200px) 1fr; + gap: 0.5rem; + align-items: start; + padding: 0.33rem 0.55rem; + border-bottom: 1px solid #eee2c8; + font-size: 0.74rem; +} + +.timing-drill-row .drill-op { + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-weight: 700; + color: #3f3f3f; +} + +.timing-drill-row .drill-meta { + color: #676255; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; +} + +.timing-drill-row.on-time { + background: #eefaf0; +} + +.timing-drill-row.early { + background: #fff1f1; +} + +.timing-drill-row.late { + background: #ffeaea; +} + +.timing-drill-row.missing { + background: #f3f3f3; +} + +.timing-drill-row.propagated { + opacity: 0.72; +} + +.timing-drill-empty { + color: #7d7668; + font-size: 0.82rem; + padding: 0.6rem; +} + +.timing-core-mini { + margin-top: 0.55rem; + border: 1px solid #dacfb5; + border-radius: 10px; + background: #fffaf0; + max-height: 210px; + overflow: auto; +} + +.timing-core-mini-head { + position: sticky; + top: 0; + z-index: 1; + background: #f4ecd9; + border-bottom: 1px solid #dacfb5; + color: #5f5548; + font-size: 0.78rem; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + padding: 0.4rem 0.55rem; + white-space: nowrap; + overflow: auto; +} + +.timing-core-mini-list { + display: flex; + flex-direction: column; +} + +.timing-core-mini-row { + padding: 0.28rem 0.55rem; + border-bottom: 1px solid #eee2c8; + font-size: 0.76rem; + color: #5a5448; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; +} + +.timing-core-mini-empty { + color: #7d7668; + font-size: 0.82rem; + padding: 0.6rem; +} + +.timing-window { + display: flex; + flex-wrap: wrap; + gap: 0.8rem; + margin: 0 0 0.55rem; +} + +.timing-window-below { + margin: 0.45rem 0 0.55rem; +} + +.timing-window-item { + gap: 0.45rem; +} + +.timing-window-item input[type="range"] { + width: min(360px, 42vw); +} + +#timingResetZoom { + height: 28px; + align-self: center; +} + +.timing-timeline-svg { + display: block; + background: #fffaf0; +} + +.timeline-axis { + stroke: #8f846d; + stroke-width: 1; +} + +.timeline-cycle-sep { + stroke: #c4b89a; + stroke-width: 1; + stroke-dasharray: 3 2; +} + +.timeline-core-sep { + stroke: #7a6f58; + stroke-width: 1.2; + stroke-dasharray: 4 3; +} + +.timeline-tick { + fill: #7a6f58; + font-size: 10px; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; +} + +.timeline-core-label { + fill: #5a5347; + font-size: 11px; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + cursor: pointer; + user-select: none; +} + +.timeline-core-label.boundary { + font-weight: 700; +} + +.timeline-core-label:hover { + fill: #2a4f9a; +} + +.timeline-core-label.focused { + fill: #1f4eb5; + font-weight: 700; + text-decoration: underline; +} + +.timeline-core-label.io-expanded { + fill: #6a2b96; + text-decoration: underline; + text-decoration-style: dashed; +} + +.timeline-lane-tag { + fill: #7f7460; + font-size: 10px; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; +} + +.timeline-io-tag-in { + fill: #2d6cdf; + font-weight: 700; +} + +.timeline-io-tag-out { + fill: #8f2ac7; + font-weight: 700; +} + +.timeline-rect { + rx: 1.6; + ry: 1.6; + shape-rendering: crispEdges; +} + +.timeline-rect.expected { + fill: #f4f4f4; + stroke: #8f8f8f; + stroke-width: 0.8; +} + +.timeline-rect.actual-ok { + fill: #2a7f62; + stroke: #1f604a; + stroke-width: 0.7; +} + +.timeline-rect.actual-bad { + fill: #d62828; + stroke: #8f1717; + stroke-width: 0.7; +} + +.timeline-rect.actual-comp-ok { + fill: #2d6cdf; + stroke: #1d4a97; + stroke-width: 0.8; + opacity: 0.84; +} + +.timeline-rect.actual-comp-bad { + fill: #9b2ce0; + stroke: #5d178a; + stroke-width: 0.85; + opacity: 0.9; +} + +.timeline-rect.missing { + fill: #f4f4f4; + stroke: #7a7a7a; + stroke-width: 1.1; + stroke-dasharray: 2 1; +} + +.timeline-rect.selected { + stroke-width: 1.8; + filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.28)); +} + +.timeline-io-bus { + stroke-width: 0.9; +} + +.timeline-io-bus-in { + fill: #deebff; + stroke: #7da3ea; +} + +.timeline-io-bus-out { + fill: #f1e1ff; + stroke: #b589dd; +} + +.timeline-io-bus-label { + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + pointer-events: none; + font-size: 7px; +} + +.timeline-io-bus-label-in { + fill: #214a9c; +} + +.timeline-io-bus-label-out { + fill: #6d2094; +} + +.timeline-rect-label { + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + pointer-events: none; + font-size: 7px; +} + +.timeline-rect-label-expected { + fill: #444; +} + +.timeline-rect-label-actual { + fill: #fff; +} + +.timeline-missing { + stroke: #7a7a7a; + stroke-width: 1.2; +} + +.timeline-link.ok { + stroke: rgba(54, 132, 103, 0.45); + stroke-width: 0.9; +} + +.timeline-link.bad { + stroke: rgba(214, 40, 40, 0.72); + stroke-width: 1.2; +} + +.timeline-link.comp-ok { + stroke: rgba(45, 108, 223, 0.5); + stroke-width: 0.9; + stroke-dasharray: 2 1; +} + +.timeline-link.comp-bad { + stroke: rgba(155, 44, 224, 0.78); + stroke-width: 1.2; + stroke-dasharray: 2 1; +} + +.timeline-legend-text { + fill: #615a4f; + font-size: 10px; +} + +.timeline-legend-exp { + fill: #fff; + stroke: #8c8c8c; +} + +.timeline-legend-act-ok { + fill: #2a7f62; +} + +.timeline-legend-act-bad { + fill: #d62828; +} + +.timeline-legend-comp-ok { + fill: #2d6cdf; +} + +.timeline-legend-comp-bad { + fill: #9b2ce0; +} + +.timeline-legend-io-in { + fill: #deebff; + stroke: #7da3ea; +} + +.timeline-legend-io-out { + fill: #f1e1ff; + stroke: #b589dd; +} + +.timeline-legend-missing { + fill: #f4f4f4; + stroke: #7a7a7a; + stroke-dasharray: 2 1; +} + +.timing-cell { + min-height: 42px; + display: flex; + flex-wrap: wrap; + gap: 0.25rem; +} + +.timing-op { + border: 1px solid #c8baa0; + border-radius: 6px; + padding: 0.1rem 0.3rem; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 0.73rem; + cursor: default; + background: #fff; +} + +.timing-op.on-time { + background: #d9f5df; + border-color: #8bbd95; +} + +.timing-op.early { + background: #ffd9d9; + border-color: #d88b8b; +} + +.timing-op.late { + background: #ffc8c8; + border-color: #d77a7a; +} + +.timing-op.missing { + background: #ececec; + border-color: #bcbcbc; +} + +.timing-op.first-divergence { + box-shadow: 0 0 0 2px rgba(96, 41, 41, 0.25) inset; + font-weight: 600; +} + +.timing-op.propagated { + opacity: 0.55; +} + +.timing-empty { + color: #8b8577; + font-size: 0.73rem; +} + @media (max-width: 860px) { #canvas { min-height: 360px; } + + .core-col { + min-width: 150px; + } + + .slot-head { + min-width: 74px; + } + + .timing-heat-btn { + min-width: 54px; + padding: 0.18rem 0.22rem; + } + + .timing-drill-row { + grid-template-columns: 1fr; + gap: 0.2rem; + } + + .timing-window-item input[type="range"] { + width: min(280px, 60vw); + } + + .timeline-core-label { + font-size: 10px; + } }