File indexing completed on 2026-11-09 10:47:33
0001 """The core occupancy plot of an Event Service job: one lane per core the
0002 job works (one single-threaded payload instance each)
0003 along the job's own clock. Measured initialization, event processing and
0004 setup/output handling have distinct segments; legacy aggregate time stays
0005 unseparated. Processing carries its output outcome (green saved, red failed; after a sudden end, light
0006 yellow the processing cut off and dark yellow the units whose output
0007 never left the node), each core's processed events at its right and their total on the closes
0008 lane, the closes on their own lane, the harness's span, the pilot's head and
0009 tail as the bare lanes before the first unit and after the last. What
0010 the job's cores were doing every minute of its life, and what they were not
0011 (swf-epicprod NODE_EVENT_DISPATCHER.md, The record). Inline SVG, no
0012 script, legible in both themes.
0013 """
0014 from django.utils.html import escape
0015 from django.utils.safestring import mark_safe
0016
0017 LEFT = 70
0018 RIGHT = 90
0019 WIDTH = 1000
0020 LANE_H = 20
0021 AXIS_H = 34
0022 TOP = 8
0023
0024 DONE = '#2e8b57'
0025 FAILED = '#c0392b'
0026 CLOSE = '#3a7bd5'
0027 HARNESS = '#8e6bbf'
0028 INTERRUPTED = '#f5d76e'
0029 UNSHIPPED = '#d49a00'
0030 RUNNING = '#7fc8a9'
0031 UNIT_COLOR = {'done': DONE, 'failed': FAILED, 'interrupted': INTERRUPTED, 'unshipped': UNSHIPPED,
0032 'running': RUNNING, 'awaiting': '#879d66'}
0033 IDLE = 'rgba(128,128,128,0.18)'
0034 INITIALIZATION = '#d97706'
0035 OVERHEAD = '#64748b'
0036 UNSEPARATED = '#8a7f91'
0037
0038
0039 def _tick_step(wall_s):
0040 """The minute step of the axis ticks for a wall of wall_s seconds."""
0041 minutes = wall_s / 60.0
0042 for step in (1, 2, 5, 10, 15, 30, 60, 120, 240):
0043 if minutes / step <= 12:
0044 return step
0045 return 480
0046
0047
0048 def slot_plot_svg(tl):
0049 """The SVG for a timeline from queries.es_slot_timeline, or ''.
0050
0051 queries.allocation_timeline gives the same shape for a batch
0052 allocation's jobs, with optional keys: ``lane_label`` (default
0053 'core'), ``closes_lane`` False to omit the closes lane, a row's
0054 ``right_text`` and the timeline's ``total_text`` in place of the
0055 events counts, and a unit's ``title`` in place of the default hover
0056 text; ``compact_lanes`` narrows the lanes and labels every eighth."""
0057 if not tl or not tl.get('wall_s'):
0058 return ''
0059 wall = float(tl['wall_s'])
0060 rows = list(tl.get('rows') or [])
0061 closes_lane = tl.get('closes_lane', True)
0062 lane_label = tl.get('lane_label') or 'core'
0063 lanes = len(rows) + (1 if closes_lane else 0)
0064 compact = bool(tl.get('compact_lanes'))
0065 LANE_H = 8 if compact else 20
0066 label_every = 8 if compact else 1
0067 height = TOP + lanes * LANE_H + AXIS_H
0068 plot_w = WIDTH - LEFT - RIGHT
0069 pad = 1 if compact else 3
0070 label_dy = LANE_H / 2 + 5 if compact else LANE_H * 0.72
0071
0072 def x(t):
0073 return LEFT + max(0.0, min(1.0, t / wall)) * plot_w
0074
0075 parts = [f'<svg viewBox="0 0 {WIDTH} {height}" width="100%" height="{height}" '
0076 f'role="img" aria-label="core occupancy" style="font-family:inherit;font-size:14px;max-width:100%">']
0077
0078 for i, row in enumerate(rows):
0079 y = TOP + i * LANE_H
0080
0081 if i % label_every == 0:
0082
0083 label = row.get('label') or f"{lane_label} {row['index']}"
0084 parts.append(f'<text x="{LEFT - 8}" y="{y + label_dy:.1f}" text-anchor="end" fill="currentColor">'
0085 f'{escape(label)}</text>')
0086 parts.append(f'<rect x="{LEFT}" y="{y + pad}" width="{plot_w:.1f}" height="{LANE_H - 2 * pad}" fill="{IDLE}"/>')
0087 for u in row['units']:
0088 x0, x1 = x(u['start_s']), x(u['end_s'])
0089 color = UNIT_COLOR.get(u['status'], FAILED)
0090 title = escape(u.get('title') or (
0091 f"{u.get('unit_id') or 'unit'}: {u['status']}, {u.get('events') or 0} events, "
0092 f"{(u['end_s'] - u['start_s']) / 60:.1f} min from {u['start_s'] / 60:.1f} min"
0093 + (f"; output shipped in close {u['close']}" if u.get('close') else '')))
0094 segments = u.get('segments')
0095 if segments:
0096 for segment in segments:
0097 phase = segment['phase']
0098 a, b = x(segment['start_s']), x(segment['end_s'])
0099 fill = (INITIALIZATION if phase == 'initialization' else
0100 OVERHEAD if phase == 'overhead' else
0101 UNSEPARATED if phase == 'unseparated' else color)
0102 label = {'initialization': 'initialization', 'overhead': 'setup / output handling',
0103 'unseparated': 'unit time; initialization and processing not separately measured',
0104 'simulation': 'simulation events', 'reconstruction': 'reconstruction events'}[phase]
0105 detail = escape(f"{label}: {segment['end_s'] - segment['start_s']:.2f} s; ")
0106 parts.append(f'<rect x="{a:.1f}" y="{y + pad}" width="{max(.2, b - a):.1f}" '
0107 f'height="{LANE_H - 2 * pad}" fill="{fill}"><title>{detail}{title}</title></rect>')
0108 else:
0109 parts.append(f'<rect x="{x0:.1f}" y="{y + pad}" width="{max(1.5, x1 - x0):.1f}" height="{LANE_H - 2 * pad}" '
0110 f'fill="{color}"><title>{title}</title></rect>')
0111
0112
0113 if x1 - x0 >= 3:
0114 parts.append(f'<line x1="{x0:.1f}" y1="{y + pad}" x2="{x0:.1f}" y2="{y + LANE_H - pad}" '
0115 f'stroke="#fff" stroke-width="1"/>')
0116
0117
0118 if 'right_text' in row:
0119 right = row['right_text']
0120 else:
0121 n = sum(int(u.get('events') or 0) for u in row['units'] if u['status'] == 'done')
0122 right = f'{n:,} ev'
0123 if right and i % label_every == 0:
0124 parts.append(f'<text x="{WIDTH - 4}" y="{y + label_dy:.1f}" text-anchor="end" fill="currentColor">'
0125 f'{escape(right)}</text>')
0126 if closes_lane:
0127
0128 y = TOP + len(rows) * LANE_H
0129 total = sum(int(u.get('events') or 0) for row in rows for u in row['units'] if u['status'] == 'done')
0130 parts.append(f'<text x="{WIDTH - 4}" y="{y + LANE_H * 0.72:.1f}" text-anchor="end" fill="currentColor" '
0131 f'font-weight="bold">{total:,} ev</text>')
0132 parts.append(f'<text x="{LEFT - 8}" y="{y + LANE_H * 0.72:.1f}" text-anchor="end" fill="currentColor">closes</text>')
0133 parts.append(f'<rect x="{LEFT}" y="{y + 3}" width="{plot_w:.1f}" height="{LANE_H - 6}" fill="{IDLE}"/>')
0134 for c in tl.get('closes') or []:
0135 x0, x1 = x(c['start_s']), x(c['end_s'])
0136 outcome = 'running' if c.get('running') else 'registered' if c.get('ok') else 'failed'
0137 title = escape(f"close {c.get('index')}: {outcome}, "
0138 f"{(c['end_s'] - c['start_s']):.0f} s from {c['start_s'] / 60:.1f} min")
0139 parts.append(f'<rect x="{x0:.1f}" y="{y + 3}" width="{max(2.0, x1 - x0):.1f}" height="{LANE_H - 6}" '
0140 f'fill="{CLOSE if c.get("ok") or c.get("running") else FAILED}"><title>{title}</title></rect>')
0141
0142 if tl.get('cut_s') is not None:
0143 xc = x(tl['cut_s'])
0144 label = 'preempted' if tl.get('cut_real') else 'cut (test)'
0145 parts.append(f'<line x1="{xc:.1f}" y1="{TOP}" x2="{xc:.1f}" y2="{TOP + lanes * LANE_H}" '
0146 f'stroke="{FAILED}" stroke-width="2"><title>{label} at {tl["cut_s"] / 60:.1f} min</title></line>')
0147
0148 h = tl.get('harness')
0149 y_axis = TOP + lanes * LANE_H
0150 if h and h.get('start_s') is not None:
0151 x0 = x(h['start_s'])
0152 x1 = x(h['end_s']) if h.get('end_s') is not None else x(wall)
0153 parts.append(f'<rect x="{x0:.1f}" y="{y_axis + 2}" width="{max(1.0, x1 - x0):.1f}" height="3" fill="{HARNESS}">'
0154 f'<title>harness running: {h["start_s"] / 60:.1f} to {(h["end_s"] or wall) / 60:.1f} min</title></rect>')
0155
0156 step = _tick_step(wall)
0157 parts.append(f'<line x1="{LEFT}" y1="{y_axis + 8}" x2="{WIDTH - RIGHT}" y2="{y_axis + 8}" stroke="currentColor" stroke-opacity="0.5"/>')
0158 m = 0
0159 while m * 60 <= wall + 1e-6:
0160 xt = x(m * 60)
0161 parts.append(f'<line x1="{xt:.1f}" y1="{y_axis + 8}" x2="{xt:.1f}" y2="{y_axis + 13}" stroke="currentColor" stroke-opacity="0.5"/>')
0162 parts.append(f'<text x="{xt:.1f}" y="{y_axis + 27}" text-anchor="middle" fill="currentColor">{m}</text>')
0163 m += step
0164 parts.append(f'<text x="{WIDTH - 4}" y="{y_axis + 27}" text-anchor="end" fill="currentColor" fill-opacity="0.7">min</text>')
0165 parts.append('</svg>')
0166 return mark_safe(''.join(parts))