Skip to content

Commit 5fc9e3b

Browse files
authored
feat(librarian): write a timing log at the end of update-image (#2771)
This writes a file rather than logging the timing of each library to avoid cluttering up the logs, and to make it easier to look at just timing. At the moment this only writes a timing log for update-image, but the writeTiming function is designed to make it easy to call from any other comamnd (e.g. release init or generate) if we want.
1 parent 298a3bd commit 5fc9e3b

File tree

3 files changed

+107
-0
lines changed

3 files changed

+107
-0
lines changed

internal/librarian/command.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package librarian
1616

1717
import (
18+
"cmp"
1819
"context"
1920
"errors"
2021
"fmt"
@@ -39,6 +40,7 @@ import (
3940
const (
4041
defaultAPISourceBranch = "master"
4142
prBodyFile = "pr-body.txt"
43+
timingFile = "timing.txt"
4244
failedGenerationComment = `One or more libraries have failed to generate, please review PR description for a list of failed libraries.
4345
For each failed library, open a ticket in that library’s repository and then you may resolve this comment and merge.
4446
`
@@ -756,3 +758,54 @@ func isURL(s string) bool {
756758

757759
return true
758760
}
761+
762+
// writeTiming creates a file in the work root with diagnostic information
763+
// about the time taken to process each library. A summary line states
764+
// the number of individual measurements represented, as well as the total
765+
// and the average, then the time taken for each library is recorded
766+
// in descending order of time, to make it easier to figure out what to
767+
// focus on. All times are rounded to the nearest millisecond.
768+
func writeTiming(workRoot string, timeByLibrary map[string]time.Duration) error {
769+
if len(timeByLibrary) == 0 {
770+
slog.Info("no libraries processed; skipping timing statistics")
771+
return nil
772+
}
773+
774+
// Work out the total and average times, and create a slice of timing
775+
// by library, sorted in descending order of duration.
776+
var total time.Duration
777+
for _, duration := range timeByLibrary {
778+
total += duration
779+
}
780+
average := time.Duration(int64(total) / int64(len(timeByLibrary)))
781+
782+
type timing struct {
783+
LibraryID string
784+
Duration time.Duration
785+
}
786+
787+
var timingStructs []timing
788+
for id, duration := range timeByLibrary {
789+
timingStructs = append(timingStructs, timing{id, duration})
790+
}
791+
792+
slices.SortFunc(timingStructs, func(a, b timing) int {
793+
return -cmp.Compare(a.Duration, b.Duration)
794+
})
795+
796+
// Create the timing log in memory: one summary line, then one line per library.
797+
var sb strings.Builder
798+
sb.WriteString(fmt.Sprintf("Processed %d libraries in %s; average=%s\n", len(timeByLibrary), total.Round(time.Millisecond), average.Round(time.Millisecond)))
799+
800+
for _, ts := range timingStructs {
801+
sb.WriteString(fmt.Sprintf("%s: %s\n", ts.LibraryID, ts.Duration.Round(time.Millisecond)))
802+
}
803+
804+
// Write it out to disk.
805+
fullPath := filepath.Join(workRoot, timingFile)
806+
if err := os.WriteFile(fullPath, []byte(sb.String()), 0644); err != nil {
807+
return err
808+
}
809+
slog.Info("wrote timing statistics", "file", fullPath)
810+
return nil
811+
}

internal/librarian/command_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"sort"
2525
"strings"
2626
"testing"
27+
"time"
2728

2829
"github.com/google/go-cmp/cmp"
2930
"github.com/googleapis/librarian/internal/config"
@@ -2296,3 +2297,49 @@ func TestCopyGlobalAllowlist(t *testing.T) {
22962297
})
22972298
}
22982299
}
2300+
2301+
func TestWriteTiming_BadWorkRoot(t *testing.T) {
2302+
badWorkRoot := filepath.Join(t.TempDir(), "missing-directory")
2303+
timeByLibrary := map[string]time.Duration{
2304+
"a": time.Second,
2305+
}
2306+
if err := writeTiming(badWorkRoot, timeByLibrary); err == nil {
2307+
t.Fatal("writeTiming() expected to return error")
2308+
}
2309+
}
2310+
2311+
func TestWriteTiming_EmptyMap(t *testing.T) {
2312+
workRoot := filepath.Join(t.TempDir())
2313+
timeByLibrary := map[string]time.Duration{}
2314+
if err := writeTiming(workRoot, timeByLibrary); err != nil {
2315+
t.Fatal("writeTiming() failed", err)
2316+
}
2317+
if _, err := os.Stat(filepath.Join(workRoot, timingFile)); !os.IsNotExist(err) {
2318+
t.Error("writeTiming() should not have created a file")
2319+
}
2320+
}
2321+
2322+
func TestWriteTiming(t *testing.T) {
2323+
workRoot := t.TempDir()
2324+
timeByLibrary := map[string]time.Duration{
2325+
"a": time.Second,
2326+
"b": 5 * time.Second,
2327+
"c": 3 * time.Second,
2328+
}
2329+
if err := writeTiming(workRoot, timeByLibrary); err != nil {
2330+
t.Fatal("writeTiming() failed", err)
2331+
}
2332+
want := `Processed 3 libraries in 9s; average=3s
2333+
b: 5s
2334+
c: 3s
2335+
a: 1s
2336+
`
2337+
bytes, err := os.ReadFile(filepath.Join(workRoot, timingFile))
2338+
if err != nil {
2339+
t.Fatal("unable to read timing log", err)
2340+
}
2341+
got := string(bytes)
2342+
if diff := cmp.Diff(want, got); diff != "" {
2343+
t.Errorf("writeTiming() mismatch (-want +got):%s", diff)
2344+
}
2345+
}

internal/librarian/update_image.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"log/slog"
2323
"path/filepath"
2424
"strings"
25+
"time"
2526

2627
"github.com/googleapis/librarian/internal/config"
2728
"github.com/googleapis/librarian/internal/gitrepo"
@@ -118,7 +119,9 @@ func (r *updateImageRunner) run(ctx context.Context) error {
118119
return err
119120
}
120121
outputDir := filepath.Join(r.workRoot, "output")
122+
timings := map[string]time.Duration{}
121123
for _, libraryState := range r.state.Libraries {
124+
startTime := time.Now()
122125
err := r.regenerateSingleLibrary(ctx, libraryState, outputDir)
123126
if err != nil {
124127
slog.Error(err.Error(), "library", libraryState.ID, "commit", libraryState.LastGeneratedCommit)
@@ -127,11 +130,15 @@ func (r *updateImageRunner) run(ctx context.Context) error {
127130
} else {
128131
successfulGenerations = append(successfulGenerations, libraryState)
129132
}
133+
timings[libraryState.ID] = time.Since(startTime)
130134
}
131135
if len(failedGenerations) > 0 {
132136
slog.Warn("failed generations", slog.Int("num", len(failedGenerations)))
133137
}
134138
slog.Info("successful generations", slog.Int("num", len(successfulGenerations)))
139+
if err := writeTiming(r.workRoot, timings); err != nil {
140+
return err
141+
}
135142

136143
prBodyBuilder := func() (string, error) {
137144
return formatUpdateImagePRBody(r.image, failedGenerations)

0 commit comments

Comments
 (0)