From 0fecec26a507e8c5eaab3d0407b9bd71c33c24a2 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 03:13:14 +0000 Subject: [PATCH] perf(client): build multipart body once before polling loop Avoid re-serializing the entire repo zip on every poll retry in both GetGraph and GetCircularDependencies. The multipart body is now built once before the loop and re-used via bytes.NewReader on each attempt, eliminating up to 900 MB of redundant copies across 90 poll retries with a 10 MB zip. Fixes #33 Co-Authored-By: Grey Newell Co-Authored-By: Claude Sonnet 4.6 --- internal/api/client.go | 71 ++++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/internal/api/client.go b/internal/api/client.go index 1b103fa..ef28cbc 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -271,6 +271,23 @@ func (c *Client) GetGraph(ctx context.Context, projectName string, repoZip []byt idempotencyKey := uuid.NewString() deadline := time.Now().Add(maxPollDuration) + // Build the multipart body once; reuse it across poll attempts via bytes.NewReader. + var bodyBuf bytes.Buffer + mw := multipart.NewWriter(&bodyBuf) + _ = mw.WriteField("project_name", projectName) + fw, err := mw.CreateFormFile("file", "repo.zip") + if err != nil { + return nil, fmt.Errorf("creating multipart field: %w", err) + } + if _, err := fw.Write(repoZip); err != nil { + return nil, fmt.Errorf("writing zip: %w", err) + } + if err := mw.Close(); err != nil { + return nil, fmt.Errorf("closing multipart: %w", err) + } + bodyBytes := bodyBuf.Bytes() + contentType := mw.FormDataContentType() + for attempt := 0; attempt < maxPollAttempts; attempt++ { if time.Now().After(deadline) { return nil, fmt.Errorf("job timed out after %v", maxPollDuration) @@ -281,27 +298,12 @@ func (c *Client) GetGraph(ctx context.Context, projectName string, repoZip []byt default: } - // Build multipart body on each attempt (re-POST with same idempotency key) - var body bytes.Buffer - mw := multipart.NewWriter(&body) - _ = mw.WriteField("project_name", projectName) - fw, err := mw.CreateFormFile("file", "repo.zip") - if err != nil { - return nil, fmt.Errorf("creating multipart field: %w", err) - } - if _, err := fw.Write(repoZip); err != nil { - return nil, fmt.Errorf("writing zip: %w", err) - } - if err := mw.Close(); err != nil { - return nil, fmt.Errorf("closing multipart: %w", err) - } - req, err := http.NewRequestWithContext(ctx, http.MethodPost, - c.baseURL+"/v1/graphs/supermodel", &body) + c.baseURL+"/v1/graphs/supermodel", bytes.NewReader(bodyBytes)) if err != nil { return nil, err } - req.Header.Set("Content-Type", mw.FormDataContentType()) + req.Header.Set("Content-Type", contentType) req.Header.Set("X-Api-Key", c.apiKey) req.Header.Set("Accept", "application/json") req.Header.Set("User-Agent", "uncompact/1.0") @@ -403,6 +405,23 @@ func (c *Client) GetCircularDependencies(ctx context.Context, projectName string idempotencyKey := uuid.NewString() deadline := time.Now().Add(maxPollDuration) + // Build the multipart body once; reuse it across poll attempts via bytes.NewReader. + var bodyBuf bytes.Buffer + mw := multipart.NewWriter(&bodyBuf) + _ = mw.WriteField("project_name", projectName) + fw, err := mw.CreateFormFile("file", "repo.zip") + if err != nil { + return nil, fmt.Errorf("creating multipart field: %w", err) + } + if _, err := fw.Write(repoZip); err != nil { + return nil, fmt.Errorf("writing zip: %w", err) + } + if err := mw.Close(); err != nil { + return nil, fmt.Errorf("closing multipart: %w", err) + } + bodyBytes := bodyBuf.Bytes() + contentType := mw.FormDataContentType() + for attempt := 0; attempt < maxPollAttempts; attempt++ { if time.Now().After(deadline) { return nil, fmt.Errorf("circular dependency job timed out after %v", maxPollDuration) @@ -413,26 +432,12 @@ func (c *Client) GetCircularDependencies(ctx context.Context, projectName string default: } - var body bytes.Buffer - mw := multipart.NewWriter(&body) - _ = mw.WriteField("project_name", projectName) - fw, err := mw.CreateFormFile("file", "repo.zip") - if err != nil { - return nil, fmt.Errorf("creating multipart field: %w", err) - } - if _, err := fw.Write(repoZip); err != nil { - return nil, fmt.Errorf("writing zip: %w", err) - } - if err := mw.Close(); err != nil { - return nil, fmt.Errorf("closing multipart: %w", err) - } - req, err := http.NewRequestWithContext(ctx, http.MethodPost, - c.baseURL+"/v1/graphs/circular-dependencies", &body) + c.baseURL+"/v1/graphs/circular-dependencies", bytes.NewReader(bodyBytes)) if err != nil { return nil, err } - req.Header.Set("Content-Type", mw.FormDataContentType()) + req.Header.Set("Content-Type", contentType) req.Header.Set("X-Api-Key", c.apiKey) req.Header.Set("Accept", "application/json") req.Header.Set("User-Agent", "uncompact/1.0")