Skip to content

Commit d08d996

Browse files
committed
[ws-manager-api, ws-daemon] Forward initializer metrics through workspace.Status.InitializerMetrics
Tool: gitpod/catfood.gitpod.cloud
1 parent 71e2b01 commit d08d996

File tree

14 files changed

+178
-47
lines changed

14 files changed

+178
-47
lines changed

components/ws-daemon/cmd/content-initializer.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
package cmd
66

77
import (
8+
"fmt"
9+
810
"github.com/spf13/cobra"
911

1012
"github.com/gitpod-io/gitpod/ws-daemon/pkg/content"
@@ -16,7 +18,14 @@ var contentInitializerCmd = &cobra.Command{
1618
Short: "fork'ed by ws-daemon to initialize content",
1719
Args: cobra.ExactArgs(0),
1820
RunE: func(cmd *cobra.Command, args []string) error {
19-
return content.RunInitializerChild()
21+
stats, err := content.RunInitializerChild()
22+
if err != nil {
23+
return err
24+
}
25+
26+
fmt.Printf(content.FormatStatsBytes(stats))
27+
28+
return nil
2029
},
2130
}
2231

components/ws-daemon/cmd/content-initializer/main.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ func main() {
2121
log.Init("content-initializer", "", true, false)
2222
tracing.Init("content-initializer")
2323

24-
err := content.RunInitializerChild()
24+
stats, err := content.RunInitializerChild()
2525
if err != nil {
2626
errfd := os.NewFile(uintptr(3), "errout")
2727
_, _ = fmt.Fprintf(errfd, err.Error())
2828

2929
os.Exit(content.FAIL_CONTENT_INITIALIZER_EXIT_CODE)
3030
}
31+
32+
fmt.Printf(content.FormatStatsBytes(stats))
3133
}

components/ws-daemon/pkg/content/initializer.go

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
package content
66

77
import (
8+
"bufio"
89
"bytes"
910
"context"
1011
"encoding/json"
1112
"errors"
13+
"fmt"
1214
"io/ioutil"
1315
"os"
1416
"os/exec"
@@ -122,21 +124,22 @@ func CollectRemoteContent(ctx context.Context, rs storage.DirectAccess, ps stora
122124
}
123125

124126
// RunInitializer runs a content initializer in a user, PID and mount namespace to isolate it from ws-daemon
125-
func RunInitializer(ctx context.Context, destination string, initializer *csapi.WorkspaceInitializer, remoteContent map[string]storage.DownloadInfo, opts RunInitializerOpts) (err error) {
127+
func RunInitializer(ctx context.Context, destination string, initializer *csapi.WorkspaceInitializer, remoteContent map[string]storage.DownloadInfo, opts RunInitializerOpts) (*csapi.InitializerMetrics, error) {
126128
//nolint:ineffassign,staticcheck
127129
span, ctx := opentracing.StartSpanFromContext(ctx, "RunInitializer")
130+
var err error
128131
defer tracing.FinishSpan(span, &err)
129132

130133
// it's possible the destination folder doesn't exist yet, because the kubelet hasn't created it yet.
131134
// If we fail to create the folder, it either already exists, or we'll fail when we try and mount it.
132135
err = os.MkdirAll(destination, 0755)
133136
if err != nil && !os.IsExist(err) {
134-
return xerrors.Errorf("cannot mkdir destination: %w", err)
137+
return nil, xerrors.Errorf("cannot mkdir destination: %w", err)
135138
}
136139

137140
init, err := proto.Marshal(initializer)
138141
if err != nil {
139-
return err
142+
return nil, err
140143
}
141144

142145
if opts.GID == 0 {
@@ -148,13 +151,13 @@ func RunInitializer(ctx context.Context, destination string, initializer *csapi.
148151

149152
tmpdir, err := os.MkdirTemp("", "content-init")
150153
if err != nil {
151-
return err
154+
return nil, err
152155
}
153156
defer os.RemoveAll(tmpdir)
154157

155158
err = os.MkdirAll(filepath.Join(tmpdir, "rootfs"), 0755)
156159
if err != nil {
157-
return err
160+
return nil, err
158161
}
159162

160163
msg := msgInitContent{
@@ -169,11 +172,11 @@ func RunInitializer(ctx context.Context, destination string, initializer *csapi.
169172
}
170173
fc, err := json.MarshalIndent(msg, "", " ")
171174
if err != nil {
172-
return err
175+
return nil, err
173176
}
174177
err = os.WriteFile(filepath.Join(tmpdir, "rootfs", "content.json"), fc, 0644)
175178
if err != nil {
176-
return err
179+
return nil, err
177180
}
178181

179182
spec := specconv.Example()
@@ -226,11 +229,11 @@ func RunInitializer(ctx context.Context, destination string, initializer *csapi.
226229

227230
fc, err = json.MarshalIndent(spec, "", " ")
228231
if err != nil {
229-
return err
232+
return nil, err
230233
}
231234
err = os.WriteFile(filepath.Join(tmpdir, "config.json"), fc, 0644)
232235
if err != nil {
233-
return err
236+
return nil, err
234237
}
235238

236239
args := []string{"--root", "state"}
@@ -243,7 +246,7 @@ func RunInitializer(ctx context.Context, destination string, initializer *csapi.
243246
if opts.OWI.InstanceID == "" {
244247
id, err := uuid.NewRandom()
245248
if err != nil {
246-
return err
249+
return nil, err
247250
}
248251
name = "init-rnd-" + id.String()
249252
} else {
@@ -256,7 +259,7 @@ func RunInitializer(ctx context.Context, destination string, initializer *csapi.
256259

257260
errIn, errOut, err := os.Pipe()
258261
if err != nil {
259-
return err
262+
return nil, err
260263
}
261264
errch := make(chan []byte, 1)
262265
go func() {
@@ -286,27 +289,49 @@ func RunInitializer(ctx context.Context, destination string, initializer *csapi.
286289
// The program has exited with an exit code != 0. If it's FAIL_CONTENT_INITIALIZER_EXIT_CODE, it was deliberate.
287290
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok && status.ExitStatus() == FAIL_CONTENT_INITIALIZER_EXIT_CODE {
288291
log.WithError(err).WithFields(opts.OWI.Fields()).WithField("errmsgsize", len(errmsg)).WithField("exitCode", status.ExitStatus()).WithField("args", args).Error("content init failed")
289-
return xerrors.Errorf(string(errmsg))
292+
return nil, xerrors.Errorf(string(errmsg))
290293
}
291294
}
292295

293-
return err
296+
return nil, err
294297
}
295298

299+
stats := parseStats(&cmdOut)
300+
301+
return stats, nil
302+
}
303+
304+
func parseStats(buf *bytes.Buffer) *csapi.InitializerMetrics {
305+
scanner := bufio.NewScanner(buf)
306+
for scanner.Scan() {
307+
line := string(scanner.Bytes())
308+
if !strings.HasPrefix(line, STATS_PREFIX) {
309+
continue
310+
}
311+
312+
b := strings.TrimSpace(strings.TrimPrefix(line, STATS_PREFIX))
313+
var stats csapi.InitializerMetrics
314+
err := json.Unmarshal([]byte(b), &stats)
315+
if err != nil {
316+
log.WithError(err).WithField("line", line).Error("cannot unmarshal stats")
317+
return nil
318+
}
319+
return &stats
320+
}
296321
return nil
297322
}
298323

299324
// RunInitializerChild is the function that's expected to run when we call `/proc/self/exe content-initializer`
300-
func RunInitializerChild() (err error) {
325+
func RunInitializerChild() (serializedStats []byte, err error) {
301326
fc, err := os.ReadFile("/content.json")
302327
if err != nil {
303-
return err
328+
return nil, err
304329
}
305330

306331
var initmsg msgInitContent
307332
err = json.Unmarshal(fc, &initmsg)
308333
if err != nil {
309-
return err
334+
return nil, err
310335
}
311336
log.Log = logrus.WithFields(initmsg.OWI)
312337

@@ -323,15 +348,15 @@ func RunInitializerChild() (err error) {
323348
var req csapi.WorkspaceInitializer
324349
err = proto.Unmarshal(initmsg.Initializer, &req)
325350
if err != nil {
326-
return err
351+
return nil, err
327352
}
328353

329354
rs := &remoteContentStorage{RemoteContent: initmsg.RemoteContent}
330355

331356
dst := initmsg.Destination
332357
initializer, err := wsinit.NewFromRequest(ctx, dst, rs, &req, wsinit.NewFromRequestOpts{ForceGitpodUserForGit: false})
333358
if err != nil {
334-
return err
359+
return nil, err
335360
}
336361

337362
initSource, stats, err := wsinit.InitializeWorkspace(ctx, dst, rs,
@@ -341,23 +366,35 @@ func RunInitializerChild() (err error) {
341366
wsinit.WithCleanSlate,
342367
)
343368
if err != nil {
344-
return err
369+
return nil, err
345370
}
346371

347372
// some workspace content may have a `/dst/.gitpod` file or directory. That would break
348373
// the workspace ready file placement (see https://github.com/gitpod-io/gitpod/issues/7694).
349374
err = wsinit.EnsureCleanDotGitpodDirectory(ctx, dst)
350375
if err != nil {
351-
return err
376+
return nil, err
352377
}
353378

354379
// Place the ready file to make Theia "open its gates"
355380
err = wsinit.PlaceWorkspaceReadyFile(ctx, dst, initSource, stats, initmsg.UID, initmsg.GID)
356381
if err != nil {
357-
return err
382+
return nil, err
358383
}
359384

360-
return nil
385+
// Serialize metrics, so we can pass them back to the caller
386+
serializedStats, err = json.Marshal(stats)
387+
if err != nil {
388+
return nil, err
389+
}
390+
391+
return serializedStats, nil
392+
}
393+
394+
const STATS_PREFIX = "STATS:"
395+
396+
func FormatStatsBytes(statsBytes []byte) string {
397+
return fmt.Sprintf("%s %s\n", STATS_PREFIX, string(statsBytes))
361398
}
362399

363400
var _ storage.DirectAccess = &remoteContentStorage{}

components/ws-daemon/pkg/controller/mock.go

Lines changed: 6 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

components/ws-daemon/pkg/controller/workspace_controller.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ func (wsc *WorkspaceController) handleWorkspaceInit(ctx context.Context, ws *wor
179179
}
180180

181181
initStart := time.Now()
182-
failure, initErr := wsc.operations.InitWorkspace(ctx, InitOptions{
182+
stats, failure, initErr := wsc.operations.InitWorkspace(ctx, InitOptions{
183183
Meta: WorkspaceMeta{
184184
Owner: ws.Spec.Ownership.Owner,
185185
WorkspaceID: ws.Spec.Ownership.WorkspaceID,
@@ -190,18 +190,25 @@ func (wsc *WorkspaceController) handleWorkspaceInit(ctx context.Context, ws *wor
190190
StorageQuota: ws.Spec.StorageQuota,
191191
})
192192

193+
initMetrics := initializerMetricsFromInitializerStats(stats)
193194
err = retry.RetryOnConflict(retryParams, func() error {
194195
if err := wsc.Get(ctx, req.NamespacedName, ws); err != nil {
195196
return err
196197
}
197198

199+
// persist init failure/success
198200
if failure != "" {
199201
log.Error(initErr, "could not initialize workspace", "name", ws.Name)
200202
ws.Status.SetCondition(workspacev1.NewWorkspaceConditionContentReady(metav1.ConditionFalse, workspacev1.ReasonInitializationFailure, failure))
201203
} else {
202204
ws.Status.SetCondition(workspacev1.NewWorkspaceConditionContentReady(metav1.ConditionTrue, workspacev1.ReasonInitializationSuccess, ""))
203205
}
204206

207+
// persist initializer metrics
208+
if initMetrics != nil {
209+
ws.Status.InitializerMetrics = initMetrics
210+
}
211+
205212
return wsc.Status().Update(ctx, ws)
206213
})
207214

@@ -218,6 +225,50 @@ func (wsc *WorkspaceController) handleWorkspaceInit(ctx context.Context, ws *wor
218225
return ctrl.Result{}, nil
219226
}
220227

228+
func initializerMetricsFromInitializerStats(stats *csapi.InitializerMetrics) *workspacev1.InitializerMetrics {
229+
if stats == nil {
230+
return nil
231+
}
232+
233+
result := workspacev1.InitializerMetrics{}
234+
for _, metric := range *stats {
235+
switch metric.Type {
236+
case "git":
237+
result.Git = &workspacev1.InitializerStepMetric{
238+
Duration: &metav1.Duration{Duration: metric.Duration},
239+
Size: metric.Size,
240+
}
241+
case "fileDownload":
242+
result.FileDownload = &workspacev1.InitializerStepMetric{
243+
Duration: &metav1.Duration{Duration: metric.Duration},
244+
Size: metric.Size,
245+
}
246+
case "snapshot":
247+
result.Snapshot = &workspacev1.InitializerStepMetric{
248+
Duration: &metav1.Duration{Duration: metric.Duration},
249+
Size: metric.Size,
250+
}
251+
case "fromBackup":
252+
result.Backup = &workspacev1.InitializerStepMetric{
253+
Duration: &metav1.Duration{Duration: metric.Duration},
254+
Size: metric.Size,
255+
}
256+
case "composite":
257+
result.Composite = &workspacev1.InitializerStepMetric{
258+
Duration: &metav1.Duration{Duration: metric.Duration},
259+
Size: metric.Size,
260+
}
261+
case "prebuild":
262+
result.Composite = &workspacev1.InitializerStepMetric{
263+
Duration: &metav1.Duration{Duration: metric.Duration},
264+
Size: metric.Size,
265+
}
266+
}
267+
}
268+
269+
return &result
270+
}
271+
221272
func (wsc *WorkspaceController) handleWorkspaceRunning(ctx context.Context, ws *workspacev1.Workspace, req ctrl.Request) (result ctrl.Result, err error) {
222273
span, ctx := opentracing.StartSpanFromContext(ctx, "handleWorkspaceRunning")
223274
defer tracing.FinishSpan(span, &err)

0 commit comments

Comments
 (0)