New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Reduce memory use for PULUMI_OPTIMIZED_CHECKPOINT_PATCH #11666
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
changes: | ||
- type: fix | ||
scope: backend/service | ||
description: "Fixes out-of-memory issues when using PULUMI_OPTIMIZED_CHECKPOINT_PATCH protocol" |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,19 +17,102 @@ package client | |
import ( | ||
"bytes" | ||
"encoding/json" | ||
"io" | ||
"math" | ||
|
||
"github.com/json-iterator/go" | ||
|
||
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype" | ||
) | ||
|
||
// Unlike json.Marshal preserves possible indentation in req.Deployment. | ||
func marshalVerbatimCheckpointRequest(req apitype.PatchUpdateVerbatimCheckpointRequest) (json.RawMessage, error) { | ||
sentinel := []byte(`"*"`) | ||
cp := req | ||
cp.UntypedDeployment = sentinel | ||
pattern, err := json.Marshal(cp) | ||
if err != nil { | ||
var jsonIterConfig = jsoniter.Config{SortMapKeys: true}.Froze() | ||
|
||
// Marshals to canonical JSON in the apitype.UntypedDeployment format. | ||
// | ||
// Optimized for large checkpoints. | ||
// | ||
// Injects newlines to allow efficient textual diffs over the JSON. Textual diffs currently use O(N^2) memory in the | ||
// number of newlines, so the injection needs to be conservative. Currently it limits to up to 1024 newlines which would | ||
// result in max 8MB memory use by the algorithm. | ||
func MarshalUntypedDeployment(deployment *apitype.DeploymentV3) (json.RawMessage, error) { | ||
var buf bytes.Buffer | ||
md := &marshalUntypedDeployment{deployment} | ||
t0yv0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if err := md.Write(&buf); err != nil { | ||
return nil, err | ||
} | ||
f := bytes.ReplaceAll(pattern, sentinel, req.UntypedDeployment) | ||
return f, nil | ||
return buf.Bytes(), nil | ||
} | ||
|
||
func marshalVerbatimCheckpointRequest(req apitype.PatchUpdateVerbatimCheckpointRequest) (json.RawMessage, error) { | ||
// Unlike encoding/json, using jsonIter here will not reindent req.UntypedDeployment, which is what is needed | ||
// for the Verbatim protocol. | ||
return jsonIterConfig.Marshal(req) | ||
} | ||
|
||
type marshalUntypedDeployment struct { | ||
deployment *apitype.DeploymentV3 | ||
} | ||
|
||
func (c *marshalUntypedDeployment) Write(w io.Writer) error { | ||
cfg := jsonIterConfig | ||
stream := cfg.BorrowStream(w) | ||
defer cfg.ReturnStream(stream) | ||
err := c.writeToStream(stream) | ||
return err | ||
} | ||
|
||
func (c *marshalUntypedDeployment) writeToStream(stream *jsoniter.Stream) error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have no comprehension of the incantation order here 😅 For example, what does There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WriteMore writes a "," .. this is a pretty low-level API. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 😆 |
||
stream.WriteObjectStart() | ||
stream.WriteObjectField("version") | ||
stream.WriteInt(3) | ||
stream.WriteMore() | ||
stream.WriteObjectField("deployment") | ||
c.writeDeploymentV3(stream) | ||
stream.WriteObjectEnd() | ||
return stream.Flush() | ||
} | ||
|
||
func (c *marshalUntypedDeployment) writeDeploymentV3(stream *jsoniter.Stream) (err error) { | ||
deployment := c.deployment | ||
stream.WriteObjectStart() | ||
stream.WriteObjectField("manifest") | ||
stream.WriteVal(deployment.Manifest) | ||
if deployment.SecretsProviders != nil { | ||
stream.WriteMore() | ||
stream.WriteObjectField("secrets_providers") | ||
stream.WriteVal(deployment.SecretsProviders) | ||
} | ||
if err = stream.Flush(); err != nil { | ||
return err | ||
} | ||
nResources := len(deployment.Resources) | ||
|
||
maxNewlines := 1024 - 2 | ||
t0yv0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
newlinePeriod := int(math.Ceil(float64(nResources) / float64(maxNewlines))) | ||
t0yv0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if nResources > 0 { | ||
stream.WriteMore() | ||
stream.WriteObjectField("resources") | ||
stream.WriteRaw("[\n") | ||
for i, r := range deployment.Resources { | ||
if i > 0 { | ||
stream.WriteRaw(",") | ||
if (nResources <= maxNewlines) || (i%newlinePeriod == 0) { | ||
stream.WriteRaw("\n") | ||
} | ||
} | ||
stream.WriteVal(r) | ||
if err = stream.Flush(); err != nil { | ||
return err | ||
} | ||
} | ||
stream.WriteRaw("\n]") | ||
} | ||
if len(deployment.PendingOperations) > 0 { | ||
stream.WriteMore() | ||
stream.WriteObjectField("pendingOperations") | ||
stream.WriteVal(deployment.PendingOperations) | ||
} | ||
stream.WriteObjectEnd() | ||
return stream.Flush() | ||
} |
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could go either way on this.
My gut says unless
jsonIterConfig
is mutable (Froze()
indicates to me it isn't), I'd throw it in a function so no one tries to mutate it.On the other hand,
Froze()
will make it apparent the minute anyone tries to mutate it (as long as they test their change), and putting this in a function will create 1 unnecessary allocation each timeWrite
is called. Probably better to leave it as is.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a strange lib. Froze() allocates state/cache/pools. This needs to be a module-level var I think for best results (reuse of the pools).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SGTM! IMO module level variables is typically an anti-pattern, except when it isn't. Which is Golang in a nutshell... :\
This is a good example of when it isn't.