Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion dsl/extdeps/render/glyphs.dag
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,14 @@ data standard_symbols: List<SymbolEntry> = [
{ id: ConnectorTeeDown, glyphs: glyphs_for_tiers(emoji: glyph(text: "┬"), unicode: glyph(text: "┬"), ascii: glyph(text: "+")), color: Dim },
{ id: ConnectorTeeUp, glyphs: glyphs_for_tiers(emoji: glyph(text: "┴"), unicode: glyph(text: "┴"), ascii: glyph(text: "+")), color: Dim },
{ id: ConnectorCornerBottomLeft, glyphs: glyphs_for_tiers(emoji: glyph(text: "╰"), unicode: glyph(text: "╰"), ascii: glyph(text: "+")), color: Dim },
{ id: ConnectorCornerTopLeft, glyphs: glyphs_for_tiers(emoji: glyph(text: "╭"), unicode: glyph(text: "╭"), ascii: glyph(text: "+")), color: Dim }
{ id: ConnectorCornerTopLeft, glyphs: glyphs_for_tiers(emoji: glyph(text: "╭"), unicode: glyph(text: "╭"), ascii: glyph(text: "+")), color: Dim },

{ id: BoxHorizontal, glyphs: glyphs_for_tiers(emoji: glyph(text: "─"), unicode: glyph(text: "─"), ascii: glyph(text: "-")), color: Dim },
{ id: BoxVertical, glyphs: glyphs_for_tiers(emoji: glyph(text: "│"), unicode: glyph(text: "│"), ascii: glyph(text: "|")), color: Dim },
{ id: BoxCornerTopLeft, glyphs: glyphs_for_tiers(emoji: glyph(text: "┌"), unicode: glyph(text: "┌"), ascii: glyph(text: "+")), color: Dim },
{ id: BoxCornerTopRight, glyphs: glyphs_for_tiers(emoji: glyph(text: "┐"), unicode: glyph(text: "┐"), ascii: glyph(text: "+")), color: Dim },
{ id: BoxCornerBottomLeft, glyphs: glyphs_for_tiers(emoji: glyph(text: "└"), unicode: glyph(text: "└"), ascii: glyph(text: "+")), color: Dim },
{ id: BoxCornerBottomRight, glyphs: glyphs_for_tiers(emoji: glyph(text: "┘"), unicode: glyph(text: "┘"), ascii: glyph(text: "+")), color: Dim }
]

fn lookup_symbol_entry(id: SymbolId) -> SymbolEntry? {
Expand Down
39 changes: 38 additions & 1 deletion dsl/extdeps/render/terminal.dag
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
module extdeps.render.terminal

import extdeps.render.glyphs { resolve_symbol }
import std.render { Frame, Line, RenderCapability, Span, Viewport, cursor_action_under }
import extdeps.render.ansi { ansi_code }
import std.render { Frame, Line, RenderCapability, Span, Viewport, cursor_action_under, repeat_string }
import std.symbols { Tier }
import std.unicode { string_display_width }
import std.width { truncate_text }
Expand Down Expand Up @@ -88,3 +89,39 @@ fn project_frame(frame: Frame, viewport: Viewport, cap: RenderCapability) -> Fra
Absent => none
}
}

fn serialize_span(span: Span, cap: RenderCapability) -> String {
let glyph = match span.style.symbol {
null => ""
sym => match resolve_symbol(id: sym, tier: cap.tier) {
Present { value: s } => s
Absent => ""
}
}
let body = concat(glyph, span.text)
if cap.color {
match span.style.color {
null => body
c => match ansi_code(c: c) {
Present { value: code } => concat(code, concat(body, "\x1b[0m"))
Absent => body
}
}
} else {
body
}
}

fn serialize_line(line: Line, cap: RenderCapability) -> String {
let pad = repeat_string(s: " ", n: line.indent * 4)
let body = fold(line.spans,
init: "",
f: (acc, span) => concat(acc, serialize_span(span: span, cap: cap))
)
concat(pad, body)
}

fn serialize_frame(frame: Frame, cap: RenderCapability) -> String {
let rendered = map(frame.lines, line => serialize_line(line: line, cap: cap))
join(rendered, "\n")
}
123 changes: 123 additions & 0 deletions dsl/gunbc/ci_render.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
module gunbc.ci_render

import std.render { Frame, Line, Span, SpanStyle, RenderCapability, repeat_string }
import std.symbols {
SemanticColor, SymbolId, Tier, Unicode,
BoxHorizontal, BoxVertical, BoxCornerTopLeft, BoxCornerTopRight, BoxCornerBottomLeft, BoxCornerBottomRight,
}
import std.unicode { string_display_width }
import std.width { truncate_text }
import std.measure { Nanosecond, nanosecond, nanosecond_count }
import extdeps.render.glyphs { resolve_symbol }
import extdeps.render.terminal { serialize_frame }

fn glyph_for(id: SymbolId, tier: Tier) -> String {
match resolve_symbol(id: id, tier: tier) {
Present { value: s } => s
Absent => ""
}
}

fn pad_to(text: String, width: Int) -> String {
let w = string_display_width(s: text)
if w >= width {
text
} else {
concat(text, repeat_string(s: " ", n: width - w))
}
}

fn plain_span(text: String) -> Span {
Span { text: text, style: SpanStyle { color: none, bold: false, italic: false, symbol: none } }
}

fn dim_span(text: String) -> Span {
Span { text: text, style: SpanStyle { color: Present { value: Dim }, bold: false, italic: false, symbol: none } }
}

fn line_of(span: Span) -> Line {
Line { spans: [span], indent: 0, max_width: none }
}

fn box_top(title: String, width: Int, tier: Tier) -> Line {
let hz = glyph_for(id: BoxHorizontal, tier: tier)
let tr = glyph_for(id: BoxCornerTopRight, tier: tier)
let prefix = concat(glyph_for(id: BoxCornerTopLeft, tier: tier), concat(hz, concat(" ", concat(title, " "))))
let dashes = width - string_display_width(s: prefix) - string_display_width(s: tr)
line_of(span: dim_span(text: concat(prefix, concat(repeat_string(s: hz, n: dashes), tr))))
}

fn box_bottom(width: Int, tier: Tier) -> Line {
let hz = glyph_for(id: BoxHorizontal, tier: tier)
let bl = glyph_for(id: BoxCornerBottomLeft, tier: tier)
let br = glyph_for(id: BoxCornerBottomRight, tier: tier)
let dashes = width - string_display_width(s: bl) - string_display_width(s: br)
line_of(span: dim_span(text: concat(bl, concat(repeat_string(s: hz, n: dashes), br))))
}

fn box_content_line(text: String, width: Int, tier: Tier) -> Line {
let v = glyph_for(id: BoxVertical, tier: tier)
let inner_budget = width - (2 * string_display_width(s: v)) - 2
let inner = pad_to(text: truncate_text(text: text, max_width: inner_budget), width: inner_budget)
line_of(span: plain_span(text: concat(v, concat(" ", concat(inner, concat(" ", v))))))
}

fn boxed_frame(title: String, body: List<String>, width: Int, tier: Tier) -> Frame {
let content = map(body, l => box_content_line(text: l, width: width, tier: tier))
let all = concat([box_top(title: title, width: width, tier: tier)], concat(content, [box_bottom(width: width, tier: tier)]))
Frame { lines: all, cursor_action: Append }
}

fn format_duration(d: Nanosecond) -> String {
let ns = nanosecond_count(n: d)
if ns < 1000 {
concat(to_string(ns), "ns")
} else if ns < 1000000 {
concat(to_string(ns / 1000), "us")
} else if ns < 1000000000 {
concat(to_string(ns / 1000000), "ms")
} else {
concat(to_string(ns / 1000000000), "s")
}
}

fn timing_cell(label: String, d: Nanosecond) -> String {
concat(label, concat(": ", format_duration(d: d)))
}

fn percentile_row(p50: Nanosecond, p90: Nanosecond, p95: Nanosecond, p99: Nanosecond, p100: Nanosecond) -> String {
concat(timing_cell(label: "p50", d: p50),
concat(" ", concat(timing_cell(label: "p90", d: p90),
concat(" ", concat(timing_cell(label: "p95", d: p95),
concat(" ", concat(timing_cell(label: "p99", d: p99),
concat(" ", timing_cell(label: "max", d: p100)))))))))
}

fn percentile_box_frame(title: String, p50: Nanosecond, p90: Nanosecond, p95: Nanosecond, p99: Nanosecond, p100: Nanosecond, width: Int, tier: Tier) -> Frame {
boxed_frame(title: title, body: [percentile_row(p50: p50, p90: p90, p95: p95, p99: p99, p100: p100)], width: width, tier: tier)
}

fn render_capability(color: Bool) -> RenderCapability {
RenderCapability { color: color, tier: Unicode, cursor_addressable: false }
}

fn percentiles_box_frame_ns(title: String, p50: Int, p90: Int, p95: Int, p99: Int, p100: Int, width: Int, tier: Tier) -> Frame {
percentile_box_frame(
title: title,
p50: nanosecond(count: p50), p90: nanosecond(count: p90), p95: nanosecond(count: p95),
p99: nanosecond(count: p99), p100: nanosecond(count: p100),
width: width, tier: tier
)
}

fn render_percentile_box(title: String, p50: Int, p90: Int, p95: Int, p99: Int, p100: Int, width: Int, color: Bool) -> String {
let cap = render_capability(color: color)
serialize_frame(
frame: percentiles_box_frame_ns(title: title, p50: p50, p90: p90, p95: p95, p99: p99, p100: p100, width: width, tier: cap.tier),
cap: cap
)
}

fn timing_histogram_frame(p50: Int, p90: Int, p95: Int, p99: Int, p100: Int, width: Int) -> Frame {
percentiles_box_frame_ns(title: "WITNESS TIMING HISTOGRAM", p50: p50, p90: p90, p95: p95, p99: p99, p100: p100, width: width, tier: Unicode)
}
7 changes: 7 additions & 0 deletions dsl/std/symbols.dag
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,10 @@ type SymbolId
| ConnectorTeeUp
| ConnectorCornerBottomLeft
| ConnectorCornerTopLeft

| BoxHorizontal
| BoxVertical
| BoxCornerTopLeft
| BoxCornerTopRight
| BoxCornerBottomLeft
| BoxCornerBottomRight
2 changes: 1 addition & 1 deletion dsl/std/unicode.dag
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ fn in_block(cp: Int, block: UnicodeBlock) -> Bool {
}

fn char_display_width(c: Char) -> DisplayWidth {
let cp = code_point(c: c)
let cp = c
let is_zero_width = contains(zero_width_codepoints, item: cp) || any(zero_width_blocks, predicate: b => in_block(cp: cp, block: b))
if is_zero_width { return ZeroWidth }
if any(wide_blocks, predicate: b => in_block(cp: cp, block: b)) { return Wide }
Expand Down
2 changes: 1 addition & 1 deletion dsl/std/width.dag
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ fn truncate_text(text: String, max_width: Int) -> String {
f: (acc, c) => if acc.used + char_width(c: c) > max_width {
acc
} else {
{ result: concat(acc.result, c), used: acc.used + char_width(c: c) }
{ result: concat(acc.result, from_code_point(c)), used: acc.used + char_width(c: c) }
}
)
state.result
Expand Down
56 changes: 56 additions & 0 deletions dsl/test/claim/ci_render_histogram_width_test.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
module test.claim.ci_render_histogram_width

import gunbc.ci_render { timing_histogram_frame, percentiles_box_frame_ns, render_percentile_box }
import extdeps.render.terminal { serialize_line, serialize_frame }
import std.render { Frame, RenderCapability }
import std.symbols { Ascii, Unicode }
import std.unicode { string_display_width }

fn no_color_cap() -> RenderCapability {
RenderCapability { color: false, tier: Unicode, cursor_addressable: false }
}

fn sample_frame(width: Int) -> Frame {
timing_histogram_frame(p50: 12, p90: 3400, p95: 56000, p99: 7800000, p100: 920000000, width: width)
}

fn all_lines_have_width(width: Int) -> Bool {
fold(sample_frame(width: width).lines,
init: true,
f: (acc, line) => acc && (string_display_width(s: serialize_line(line: line, cap: no_color_cap())) == width)
)
}

fn has_three_lines(width: Int) -> Bool {
count(sample_frame(width: width).lines) == 3
}

fn exact_small_box() -> Bool {
render_percentile_box(title: "X", p50: 1, p90: 2, p95: 3, p99: 4, p100: 5, width: 30, color: false) ==
"┌─ X ────────────────────────┐\n│ p50: 1ns p90: 2ns p95: 3 │\n└────────────────────────────┘"
}

fn ascii_box(width: Int) -> Frame {
percentiles_box_frame_ns(title: "X", p50: 1, p90: 2, p95: 3, p99: 4, p100: 5, width: width, tier: Ascii)
}

fn ascii_width_holds() -> Bool {
fold(ascii_box(width: 48).lines,
init: true,
f: (acc, line) => acc && (string_display_width(s: serialize_line(line: line, cap: no_color_cap())) == 48)
)
}

fn tier_swaps_glyphs() -> Bool {
serialize_frame(frame: ascii_box(width: 48), cap: no_color_cap()) !=
serialize_frame(frame: timing_histogram_frame(p50: 1, p90: 2, p95: 3, p99: 4, p100: 5, width: 48), cap: no_color_cap())
}

test fn ci_render_histogram_width_witnesses() -> Bool {
all_lines_have_width(width: 80) &&
all_lines_have_width(width: 64) &&
has_three_lines(width: 80) &&
exact_small_box() &&
ascii_width_holds() &&
tier_swaps_glyphs()
}
Loading