perf: cap the History list and fold column widths incrementally
History was appended to on every recorded run and never trimmed, and every event re-sorted the whole slice and re-measured the Job, Detail and Log columns across every row. The per-run cost therefore grew with the number of rows, in exactly the mode the app is designed for: left in the tray for days. The session History now keeps the newest maxHistoryRows (1000) records, the way maxJobLogs caps a job's own activity list, and drops the oldest from the front, zeroing the tail so a dropped record's full captured output is not kept alive by the backing array. Column widths move into a historyLog value that folds each new record into the current maxima instead of rescanning. Widths only grow within a theme, so a column never narrows when a record ages out; a theme change is the one case that still rescans, because every stored width was measured at the old text size. Measured with a throwaway benchmark over 5000 accumulated records: one refresh went from 15.8 ms to 0.9 ms. At the new cap the full width rescan alone costs 1.5 ms, so both halves of the fix carry weight. Plan item 6 of docs/PROJECT_REVIEW_PLAN.md (finding 3.1). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,13 @@ the app icon (experimental).**
|
|||||||
longer than its own interval no longer accumulates an unbounded backlog that
|
longer than its own interval no longer accumulates an unbounded backlog that
|
||||||
then runs back-to-back indefinitely. The job details pane now shows the
|
then runs back-to-back indefinitely. The job details pane now shows the
|
||||||
queued-run count (", N queued") whenever it is non-zero.
|
queued-run count (", N queued") whenever it is non-zero.
|
||||||
|
- The History tab no longer grows without bound: it keeps the newest 1000
|
||||||
|
records and drops the oldest, the way a job's own activity list is capped.
|
||||||
|
Column widths are also folded in one record at a time instead of being
|
||||||
|
re-measured across every row on every event, so recording a run no longer
|
||||||
|
gets slower the longer the app has been running. Measured on 5000 accumulated
|
||||||
|
records, one History redraw went from **15.8 ms to 0.9 ms**; at the new cap
|
||||||
|
the width rescan alone accounted for 1.5 ms of every redraw.
|
||||||
|
|
||||||
**Jobs:**
|
**Jobs:**
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,14 @@ change to their shape has to stay compatible on its own.
|
|||||||
- **History tab is session-only.** `JobRuntime.Logs` exists only in memory for the
|
- **History tab is session-only.** `JobRuntime.Logs` exists only in memory for the
|
||||||
current process. Log files on disk feed aggregate statistics via `SeedStats`
|
current process. Log files on disk feed aggregate statistics via `SeedStats`
|
||||||
only. See [ARCHITECTURE.md](ARCHITECTURE.md).
|
only. See [ARCHITECTURE.md](ARCHITECTURE.md).
|
||||||
|
- **History is capped and its columns only widen.** The tab keeps the newest
|
||||||
|
`maxHistoryRows` records and drops the oldest, the way `maxJobLogs` caps a
|
||||||
|
job's own activity list — an app left in the tray records thousands of runs a
|
||||||
|
day, each carrying the run's full captured output. Column widths are folded in
|
||||||
|
one record at a time instead of rescanned from every row, so a column never
|
||||||
|
narrows when a record ages out: the rows on screen were laid out against the
|
||||||
|
wider value. A theme change is the one case that rescans, because every stored
|
||||||
|
width was measured at the old text size.
|
||||||
- Several tests share a coverage profile with another test on purpose, and a few
|
- Several tests share a coverage profile with another test on purpose, and a few
|
||||||
functions sit at 0% on purpose. Both lists live in
|
functions sit at 0% on purpose. Both lists live in
|
||||||
[TESTS.md](TESTS.md) — check them before reporting a test as redundant or a
|
[TESTS.md](TESTS.md) — check them before reporting a test as redundant or a
|
||||||
|
|||||||
@@ -494,6 +494,10 @@ column-width behaviour of the assembled table.
|
|||||||
| `TestHistoryCellTemplateIsPlainText` | Verifies the cell template already carries the zero `TextStyle`, since the per-cell assignment that used to reset it is gone. |
|
| `TestHistoryCellTemplateIsPlainText` | Verifies the cell template already carries the zero `TextStyle`, since the per-cell assignment that used to reset it is gone. |
|
||||||
| `TestTextColumnWidthClamps` | Covers the three shapes of `textColumnWidth`: below the minimum, in range, and capped at the maximum. |
|
| `TestTextColumnWidthClamps` | Covers the three shapes of `textColumnWidth`: below the minimum, in range, and capped at the maximum. |
|
||||||
| `TestHistoryColumnsFitTheirContent` | Verifies every column is at least as wide as its widest known or present value, at the default text size and at a scaled theme. |
|
| `TestHistoryColumnsFitTheirContent` | Verifies every column is at least as wide as its widest known or present value, at the default text size and at a scaled theme. |
|
||||||
|
| `TestHistoryLogCapsRecords` | Regression guard for the unbounded History list: the log keeps the newest `maxHistoryRows` records, drops the oldest from the front, and trims a list handed in already over the cap. |
|
||||||
|
| `TestHistoryLogWidthsMatchAFullScan` | Verifies the incremental column widths equal a full rescan while every measured record is still present — the cheaper path must not clip what the old one showed. |
|
||||||
|
| `TestHistoryLogWidthsDoNotShrinkWhenRecordsAgeOut` | Verifies a column keeps its width after the record that set it is dropped by the cap, since the rows on screen were laid out against it. |
|
||||||
|
| `TestHistoryLogRescansOnThemeChange` | Verifies a theme change falls back to a full rescan, the one case the incremental fold cannot handle because every stored width was measured at the old text size. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+111
-20
@@ -102,23 +102,113 @@ const historyTimeSample = "2026-01-02 15:04:05"
|
|||||||
// Detail and Log are free text, so their width tracks the values actually
|
// Detail and Log are free text, so their width tracks the values actually
|
||||||
// present, bounded the same way the Log column always was.
|
// present, bounded the same way the Log column always was.
|
||||||
func historyColumnWidths(rows []event) [6]float32 {
|
func historyColumnWidths(rows []event) [6]float32 {
|
||||||
jobNames := make([]string, 0, len(rows))
|
var content [3][]string
|
||||||
details := make([]string, 0, len(rows))
|
for i := range content {
|
||||||
logNames := make([]string, 0, len(rows))
|
content[i] = make([]string, 0, len(rows))
|
||||||
|
}
|
||||||
for _, current := range rows {
|
for _, current := range rows {
|
||||||
jobNames = append(jobNames, current.JobName)
|
for i, value := range historyContentValues(current) {
|
||||||
details = append(details, current.Detail)
|
content[i] = append(content[i], value)
|
||||||
logNames = append(logNames, logFileName(current.LogFile))
|
}
|
||||||
}
|
}
|
||||||
min, max := textColumnMinWidth(), textColumnMaxWidth()
|
min, max := textColumnMinWidth(), textColumnMaxWidth()
|
||||||
return [6]float32{
|
widths := [6]float32{
|
||||||
textWidth(historyTimeSample) + cellPadding(),
|
0: textWidth(historyTimeSample) + cellPadding(),
|
||||||
textColumnWidth(historyTriggerSamples, min, max),
|
1: textColumnWidth(historyTriggerSamples, min, max),
|
||||||
textColumnWidth(jobNames, min, max),
|
3: textColumnWidth(historyStateSamples, min, max),
|
||||||
textColumnWidth(historyStateSamples, min, max),
|
|
||||||
textColumnWidth(details, min, max),
|
|
||||||
textColumnWidth(logNames, min, max),
|
|
||||||
}
|
}
|
||||||
|
for i, col := range historyContentCols {
|
||||||
|
widths[col] = textColumnWidth(content[i], min, max)
|
||||||
|
}
|
||||||
|
return widths
|
||||||
|
}
|
||||||
|
|
||||||
|
// maxHistoryRows caps the session History list, the way app.maxJobLogs caps a
|
||||||
|
// job's own activity list. History is never persisted and every record carries
|
||||||
|
// the run's full captured output, so an app left running in the tray — the mode
|
||||||
|
// GoSentry is designed for — would otherwise hold every record of every run
|
||||||
|
// forever, and pay a full resort plus a full column-width rescan on each new
|
||||||
|
// one. One job on @every 10s produces ~8 600 records a day.
|
||||||
|
const maxHistoryRows = 1000
|
||||||
|
|
||||||
|
// historyLog is the session History: the capped record list plus the column
|
||||||
|
// widths measured from it. It exists so the widths can be folded in one record
|
||||||
|
// at a time instead of being recomputed from every row on every event, which
|
||||||
|
// is what made the per-event cost grow with the number of rows.
|
||||||
|
type historyLog struct {
|
||||||
|
records []event
|
||||||
|
widths [6]float32
|
||||||
|
// textSize and padding are the theme metrics widths were last measured at.
|
||||||
|
// A theme change invalidates every measurement, so it forces a full rescan
|
||||||
|
// rather than folding new records into stale numbers.
|
||||||
|
textSize float32
|
||||||
|
padding float32
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHistoryLog(records []event) *historyLog {
|
||||||
|
h := &historyLog{records: trimHistory(records)}
|
||||||
|
h.rescan()
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// trimHistory drops the oldest records past the cap. The tail of the backing
|
||||||
|
// array is zeroed because a dropped record holds the run's whole output, which
|
||||||
|
// would otherwise stay reachable until the slice happens to be reallocated.
|
||||||
|
func trimHistory(records []event) []event {
|
||||||
|
if len(records) <= maxHistoryRows {
|
||||||
|
return records
|
||||||
|
}
|
||||||
|
kept := copy(records, records[len(records)-maxHistoryRows:])
|
||||||
|
for i := kept; i < len(records); i++ {
|
||||||
|
records[i] = event{}
|
||||||
|
}
|
||||||
|
return records[:kept]
|
||||||
|
}
|
||||||
|
|
||||||
|
// add appends one record and widens any content-measured column the record
|
||||||
|
// does not fit. Widths only ever grow within a theme: a column is never
|
||||||
|
// narrowed when a record ages out, because the rows still on screen were laid
|
||||||
|
// out against the wider value.
|
||||||
|
func (h *historyLog) add(record event) {
|
||||||
|
h.records = trimHistory(append(h.records, record))
|
||||||
|
if h.stale() {
|
||||||
|
h.rescan()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
min, max := textColumnMinWidth(), textColumnMaxWidth()
|
||||||
|
for i, value := range historyContentValues(record) {
|
||||||
|
if width := textColumnWidth([]string{value}, min, max); width > h.widths[historyContentCols[i]] {
|
||||||
|
h.widths[historyContentCols[i]] = width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// columnWidths returns the widths to apply to the table, rescanning every
|
||||||
|
// record only when the theme's text metrics have changed since the last scan.
|
||||||
|
func (h *historyLog) columnWidths() [6]float32 {
|
||||||
|
if h.stale() {
|
||||||
|
h.rescan()
|
||||||
|
}
|
||||||
|
return h.widths
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *historyLog) stale() bool {
|
||||||
|
return theme.TextSize() != h.textSize || cellPadding() != h.padding
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *historyLog) rescan() {
|
||||||
|
h.textSize, h.padding = theme.TextSize(), cellPadding()
|
||||||
|
h.widths = historyColumnWidths(h.records)
|
||||||
|
}
|
||||||
|
|
||||||
|
// historyContentCols are the columns whose width follows the values actually
|
||||||
|
// present, in the order historyContentValues returns them. Both the
|
||||||
|
// incremental fold in add and the full scan in historyColumnWidths go through
|
||||||
|
// this pair, so they cannot disagree about which columns follow content.
|
||||||
|
var historyContentCols = [3]int{2, 4, 5}
|
||||||
|
|
||||||
|
func historyContentValues(record event) [3]string {
|
||||||
|
return [3]string{record.JobName, record.Detail, logFileName(record.LogFile)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// historyHeader is a bold tappable label used in the History table header row.
|
// historyHeader is a bold tappable label used in the History table header row.
|
||||||
@@ -156,7 +246,7 @@ func (h *historyHeader) SetText(text string) {
|
|||||||
// Time caption is built per update because it carries the sort direction arrow.
|
// Time caption is built per update because it carries the sort direction arrow.
|
||||||
var historyHeaders = [...]string{"Time", "Trigger", "Job", "State", "Detail", "Log"}
|
var historyHeaders = [...]string{"Time", "Trigger", "Job", "State", "Detail", "Log"}
|
||||||
|
|
||||||
func newHistoryView(events *[]event) (*fyne.Container, func()) {
|
func newHistoryView(log *historyLog) (*fyne.Container, func()) {
|
||||||
descending := false
|
descending := false
|
||||||
headerText := func(id widget.TableCellID) string {
|
headerText := func(id widget.TableCellID) string {
|
||||||
if id.Row < 0 && id.Col == 0 {
|
if id.Row < 0 && id.Col == 0 {
|
||||||
@@ -179,7 +269,7 @@ func newHistoryView(events *[]event) (*fyne.Container, func()) {
|
|||||||
// per redraw: at build time, on a sort toggle, and from refresh().
|
// per redraw: at build time, on a sort toggle, and from refresh().
|
||||||
var rows []event
|
var rows []event
|
||||||
resort := func() {
|
resort := func() {
|
||||||
rows = append(rows[:0], (*events)...)
|
rows = append(rows[:0], log.records...)
|
||||||
sort.SliceStable(rows, func(left int, right int) bool {
|
sort.SliceStable(rows, func(left int, right int) bool {
|
||||||
if descending {
|
if descending {
|
||||||
return rows[left].Time > rows[right].Time
|
return rows[left].Time > rows[right].Time
|
||||||
@@ -224,16 +314,17 @@ func newHistoryView(events *[]event) (*fyne.Container, func()) {
|
|||||||
table.Unselect(id)
|
table.Unselect(id)
|
||||||
}
|
}
|
||||||
setColumnWidths := func() {
|
setColumnWidths := func() {
|
||||||
for col, width := range historyColumnWidths(rows) {
|
for col, width := range log.columnWidths() {
|
||||||
table.SetColumnWidth(col, width)
|
table.SetColumnWidth(col, width)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setColumnWidths()
|
setColumnWidths()
|
||||||
|
|
||||||
// refresh re-reads the event list into the sorted snapshot and recomputes
|
// refresh re-reads the event list into the sorted snapshot and re-applies
|
||||||
// every content-fit column width before redrawing, so newly recorded events
|
// the column widths before redrawing, so newly recorded events appear in
|
||||||
// appear in the current sort order and longer values widen their column
|
// the current sort order and longer values widen their column instead of
|
||||||
// instead of being truncated.
|
// being truncated. The widths come from historyLog, which folded each new
|
||||||
|
// record in as it arrived — this does not rescan every row.
|
||||||
refresh := func() {
|
refresh := func() {
|
||||||
resort()
|
resort()
|
||||||
setColumnWidths()
|
setColumnWidths()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -144,7 +145,8 @@ func TestHistorySortToggleKeepsRowsInSync(t *testing.T) {
|
|||||||
{Time: "2026-06-01 11:00:00", JobName: "B"},
|
{Time: "2026-06-01 11:00:00", JobName: "B"},
|
||||||
{Time: "2026-06-01 12:00:00", JobName: "C"},
|
{Time: "2026-06-01 12:00:00", JobName: "C"},
|
||||||
}
|
}
|
||||||
content, refresh := newHistoryView(&events)
|
log := newHistoryLog(events)
|
||||||
|
content, refresh := newHistoryView(log)
|
||||||
table, ok := content.Objects[0].(*widget.Table)
|
table, ok := content.Objects[0].(*widget.Table)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Fatal("history view does not wrap a table")
|
t.Fatal("history view does not wrap a table")
|
||||||
@@ -192,7 +194,7 @@ func TestHistorySortToggleKeepsRowsInSync(t *testing.T) {
|
|||||||
|
|
||||||
// A new run arrives while the table is sorted newest-first: it must be
|
// A new run arrives while the table is sorted newest-first: it must be
|
||||||
// counted and placed in the order currently on screen, not the build-time one.
|
// counted and placed in the order currently on screen, not the build-time one.
|
||||||
events = append(events, event{Time: "2026-06-01 13:00:00", JobName: "D"})
|
log.add(event{Time: "2026-06-01 13:00:00", JobName: "D"})
|
||||||
refresh()
|
refresh()
|
||||||
assertOrder("descending after refresh", "D", "C", "B", "A")
|
assertOrder("descending after refresh", "D", "C", "B", "A")
|
||||||
|
|
||||||
@@ -207,8 +209,7 @@ func TestHistoryCellTemplateIsPlainText(t *testing.T) {
|
|||||||
testApp := test.NewApp()
|
testApp := test.NewApp()
|
||||||
defer testApp.Quit()
|
defer testApp.Quit()
|
||||||
|
|
||||||
var events []event
|
content, _ := newHistoryView(newHistoryLog(nil))
|
||||||
content, _ := newHistoryView(&events)
|
|
||||||
table := content.Objects[0].(*widget.Table)
|
table := content.Objects[0].(*widget.Table)
|
||||||
label, ok := table.CreateCell().(*widget.Label)
|
label, ok := table.CreateCell().(*widget.Label)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -292,6 +293,96 @@ func TestHistoryColumnsFitTheirContent(t *testing.T) {
|
|||||||
check("scaled theme")
|
check("scaled theme")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestHistoryLogCapsRecords is the regression guard for the unbounded History
|
||||||
|
// list: an app left in the tray records thousands of runs a day, each carrying
|
||||||
|
// the run's whole captured output, so the list must drop the oldest instead of
|
||||||
|
// growing forever.
|
||||||
|
func TestHistoryLogCapsRecords(t *testing.T) {
|
||||||
|
testApp := test.NewApp()
|
||||||
|
defer testApp.Quit()
|
||||||
|
|
||||||
|
log := newHistoryLog(nil)
|
||||||
|
for i := 0; i < maxHistoryRows+25; i++ {
|
||||||
|
log.add(event{Time: "t", JobName: "Job " + strconv.Itoa(i)})
|
||||||
|
}
|
||||||
|
if len(log.records) != maxHistoryRows {
|
||||||
|
t.Fatalf("record count = %d, want capped at %d", len(log.records), maxHistoryRows)
|
||||||
|
}
|
||||||
|
if got, want := log.records[0].JobName, "Job 25"; got != want {
|
||||||
|
t.Errorf("oldest kept record = %q, want %q — the cap must drop from the front", got, want)
|
||||||
|
}
|
||||||
|
last := log.records[len(log.records)-1].JobName
|
||||||
|
if want := "Job " + strconv.Itoa(maxHistoryRows+24); last != want {
|
||||||
|
t.Errorf("newest record = %q, want %q", last, want)
|
||||||
|
}
|
||||||
|
// A list handed in above the cap is trimmed too, not only one grown into it.
|
||||||
|
oversized := make([]event, maxHistoryRows+10)
|
||||||
|
if got := len(newHistoryLog(oversized).records); got != maxHistoryRows {
|
||||||
|
t.Errorf("pre-filled log length = %d, want %d", got, maxHistoryRows)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHistoryLogWidthsMatchAFullScan pins the incremental column widths: while
|
||||||
|
// every measured record is still in the list, folding each one in as it
|
||||||
|
// arrives must give exactly what rescanning every row would, or the cheaper
|
||||||
|
// path would clip values the old one showed.
|
||||||
|
func TestHistoryLogWidthsMatchAFullScan(t *testing.T) {
|
||||||
|
testApp := test.NewApp()
|
||||||
|
defer testApp.Quit()
|
||||||
|
|
||||||
|
log := newHistoryLog(nil)
|
||||||
|
for _, record := range []event{
|
||||||
|
{Time: "1", JobName: "A", Detail: "short", LogFile: `/logs/a.log`},
|
||||||
|
{Time: "2", JobName: "A moderately long job name", Detail: "a longer detail message", LogFile: `/logs/20260601-120000_SomeJobName.log`},
|
||||||
|
{Time: "3", JobName: "B", Detail: "s", LogFile: `/logs/b.log`},
|
||||||
|
} {
|
||||||
|
log.add(record)
|
||||||
|
}
|
||||||
|
if got, want := log.columnWidths(), historyColumnWidths(log.records); got != want {
|
||||||
|
t.Errorf("incremental widths = %v, want the full-scan widths %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHistoryLogWidthsDoNotShrinkWhenRecordsAgeOut covers the other half of the
|
||||||
|
// rule: widths only grow. Dropping the record that set a column's width must
|
||||||
|
// not narrow the column, because the rows on screen were laid out against it.
|
||||||
|
func TestHistoryLogWidthsDoNotShrinkWhenRecordsAgeOut(t *testing.T) {
|
||||||
|
testApp := test.NewApp()
|
||||||
|
defer testApp.Quit()
|
||||||
|
|
||||||
|
log := newHistoryLog(nil)
|
||||||
|
log.add(event{Time: "1", JobName: "A job name long enough to widen its column"})
|
||||||
|
widest := log.columnWidths()[2]
|
||||||
|
for i := 0; i < maxHistoryRows; i++ {
|
||||||
|
log.add(event{Time: "t", JobName: "x"})
|
||||||
|
}
|
||||||
|
if got := log.columnWidths()[2]; got != widest {
|
||||||
|
t.Errorf("Job column width = %v after the wide record aged out, want it held at %v", got, widest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHistoryLogRescansOnThemeChange guards the one case the incremental fold
|
||||||
|
// cannot handle: every stored width was measured at the old text size, so a
|
||||||
|
// theme change has to fall back to a full rescan.
|
||||||
|
func TestHistoryLogRescansOnThemeChange(t *testing.T) {
|
||||||
|
testApp := test.NewApp()
|
||||||
|
defer testApp.Quit()
|
||||||
|
|
||||||
|
log := newHistoryLog([]event{
|
||||||
|
{Time: "1", JobName: "A moderately long job name", Detail: "a longer detail message"},
|
||||||
|
})
|
||||||
|
before := log.columnWidths()
|
||||||
|
|
||||||
|
testApp.Settings().SetTheme(test.NewTheme())
|
||||||
|
after := log.columnWidths()
|
||||||
|
if after == before {
|
||||||
|
t.Fatal("widths unchanged after a theme change; the fixture theme must alter text metrics")
|
||||||
|
}
|
||||||
|
if want := historyColumnWidths(log.records); after != want {
|
||||||
|
t.Errorf("widths after theme change = %v, want the rescanned %v", after, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewEventUsesConsistentTimestampShape(t *testing.T) {
|
func TestNewEventUsesConsistentTimestampShape(t *testing.T) {
|
||||||
ev := newEvent(1, "Job", "OK", "detail")
|
ev := newEvent(1, "Job", "OK", "detail")
|
||||||
if _, err := time.Parse("2006-01-02 15:04:05", ev.Time); err != nil {
|
if _, err := time.Parse("2006-01-02 15:04:05", ev.Time); err != nil {
|
||||||
|
|||||||
@@ -34,11 +34,11 @@ func newMainView(w fyne.Window, svc *app.Service) (fyne.CanvasObject, func(time.
|
|||||||
initialRuntimes[j.ID] = rt
|
initialRuntimes[j.ID] = rt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
events := collectActivity(initialJobs, initialRuntimes)
|
events := newHistoryLog(collectActivity(initialJobs, initialRuntimes))
|
||||||
|
|
||||||
jobsPanel, refreshJobsView := newJobsView(w, svc)
|
jobsPanel, refreshJobsView := newJobsView(w, svc)
|
||||||
|
|
||||||
history, refreshHistory := newHistoryView(&events)
|
history, refreshHistory := newHistoryView(events)
|
||||||
recordStartup := func(duration time.Duration, windowShown bool) {
|
recordStartup := func(duration time.Duration, windowShown bool) {
|
||||||
// Startup is recorded as an in-memory History event instead of being
|
// Startup is recorded as an in-memory History event instead of being
|
||||||
// persisted into jobs.json. It is session diagnostics, not durable job
|
// persisted into jobs.json. It is session diagnostics, not durable job
|
||||||
@@ -48,7 +48,7 @@ func newMainView(w fyne.Window, svc *app.Service) (fyne.CanvasObject, func(time.
|
|||||||
if !windowShown {
|
if !windowShown {
|
||||||
detail = "Started in tray in " + duration.Round(time.Millisecond).String()
|
detail = "Started in tray in " + duration.Round(time.Millisecond).String()
|
||||||
}
|
}
|
||||||
events = append(events, newEvent(0, "Application", "Started", detail))
|
events.add(newEvent(0, "Application", "Started", detail))
|
||||||
refreshHistory()
|
refreshHistory()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ func newMainView(w fyne.Window, svc *app.Service) (fyne.CanvasObject, func(time.
|
|||||||
jobsLoaded, isJobsLoaded := ev.(app.JobsLoaded)
|
jobsLoaded, isJobsLoaded := ev.(app.JobsLoaded)
|
||||||
fyne.Do(func() {
|
fyne.Do(func() {
|
||||||
if isRecorded {
|
if isRecorded {
|
||||||
events = append(events, recorded.Record)
|
events.add(recorded.Record)
|
||||||
r := recorded.Record
|
r := recorded.Record
|
||||||
if r.State == "Failed" &&
|
if r.State == "Failed" &&
|
||||||
(r.Trigger == "Manual" || r.Trigger == "Schedule") &&
|
(r.Trigger == "Manual" || r.Trigger == "Schedule") &&
|
||||||
@@ -96,13 +96,13 @@ func newMainView(w fyne.Window, svc *app.Service) (fyne.CanvasObject, func(time.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if isError {
|
if isError {
|
||||||
events = append(events, newEvent(0, "Service", "Error", errOccurred.Err.Error()))
|
events.add(newEvent(0, "Service", "Error", errOccurred.Err.Error()))
|
||||||
}
|
}
|
||||||
if isJobsLoaded {
|
if isJobsLoaded {
|
||||||
// Selecting an existing jobs file replaces the job list without a
|
// Selecting an existing jobs file replaces the job list without a
|
||||||
// prompt, so History carries the receipt: how many jobs, from where.
|
// prompt, so History carries the receipt: how many jobs, from where.
|
||||||
detail := strconv.Itoa(jobsLoaded.Count) + " jobs from " + jobsLoaded.Path
|
detail := strconv.Itoa(jobsLoaded.Count) + " jobs from " + jobsLoaded.Path
|
||||||
events = append(events, newEvent(0, "Service", "Jobs loaded", detail))
|
events.add(newEvent(0, "Service", "Jobs loaded", detail))
|
||||||
}
|
}
|
||||||
refresh()
|
refresh()
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user