Skip to content
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

progressui: adds a json output that shows raw events for the solver status #4113

Merged
merged 1 commit into from
Aug 25, 2023

Conversation

jsternberg
Copy link
Collaborator

@jsternberg jsternberg commented Aug 7, 2023

This adds an additional display output for the progress indicator to
support a json output. It refactors the progressui package a bit to add
a new method that takes in a SolveStatusDisplay. This
SolveStatusDisplay can be created by the user using NewDisplay with
the various modes as input parameters.

The json output will print the status updates and log messages for the
vertexes that have had updates. Each JSON blob has the same format and
it matches closely with the raw underlying status updates. The display
will still attempt to regulate the number of messages that come through
and will batch various events together if events are sent too quickly
according to the default parameters for rate limiting set in the
progressui package.

At the moment, there is no public API for creating your own
SolveStatusDisplay and each of the display implementations borrow
heavily from each other. In the future, it is possible this API could be
expanded. If you need to create a custom progress bar that requires more
than what this package offers, I'd probably recommend just copying the
package and acting on the SolveStatus channel directly.

Related to docker/buildx#178.

Comment on lines 383 to 384
// Skip empty messages for the json printer.
return nil
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is the correct decision since it might be making a choice for the user that we don't want to make.

@jsternberg
Copy link
Collaborator Author

There don't appear to be current tests for the raw output. I can try to add some tests for that if it's desired or if there's a location where these tests might be I'm happy to add new ones for the JSON format.

Copy link
Member

@tonistiigi tonistiigi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enable this from the buildctl side as well. For testing I think it also makes sense to test it via buildctl test as it is mostly client-side behavior. Take a look at buildctl_test.go for some examples of such integration tests.

@@ -42,6 +43,13 @@ func WithDesc(text string, console string) DisplaySolveStatusOpt {
}
}

// WithJSONPrinter prints each line of output as a JSON stream instead of as raw text.
func WithJSONPrinter() DisplaySolveStatusOpt {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should just be a mode parameter in DisplaySolveStatus so the output doesn't only depend on passing the console.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we might want to avoid the mode parameter. I had only seen mentions of mode in buildx and any mentions of auto, tty, or plain are gone by the time we reach this code. From what I could find, both the plain text and tty outputs both use DisplaySolveStatus and which gets used depends on whether console.Console is nil or not.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it is not very nice library if there are 3 distinct modes then to configure 2 of them you would use the console object and for the third there is a special method. And we will probably add even more modes in the future.

Buildctl also supports these progress modes so it is not buildx-only concept. We can just combine all the usage so it is defined in one place (basically push it in a little bit lower level).

// jsonPrinter prints the text in JSON format.
type jsonPrinter struct{}

func (*jsonPrinter) Print(w io.Writer, index int, msg string) error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSON printing should keep the original formatting. Instead of converting to index-msg pairs and printing these as JSON it should print the original Vertex/VertexLog etc objects as soon as they appear.

Another logic would be to try to avoid partial events and only print vertex once it has completed. This would probably be easier to analyze from caller side.

Eg.

{
  "digest": "",
  "name": "",
  "intervals": [
    {start:, complete:, cached:, error:}
  ],
  "logs": [
    {timestamp:, msg}
  ]
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at my previous example, it is hard to know when last interval has completed and that would basically mean that all steps would need to be completed before logs can appear that is not ideal in some cases.

Instead we could do something similar but only once per interval. So every time there is a completed event for vertex a new record is written out (with all the logs for that vertex). If step has multiple run intervals, there can be multiple objects with same digest.

We could also just do the raw event objects instead but maybe in that case we should call this rawjson.

Copy link
Collaborator Author

@jsternberg jsternberg Aug 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mocked together an output format that I think will work well. As an example:

{"digest":"sha256:4fdbd27f357e8ca142a8dfde9064febf498ca55d6ac098fe79444f605d732794","name":"[xx 1/1] FROM docker.io/tonistiigi/xx:1.2.1@sha256:8879a398dedf0aadaacfbd332b29ff2f84bc39ae6d4e9c0a1109db27ac5ba012","statuses":[{"ID":"resolve docker.io/tonistiigi/xx:1.2.1@sha256:8879a398dedf0aadaacfbd332b29ff2f84bc39ae6d4e9c0a1109db27ac5ba012","Vertex":"sha256:4fdbd27f357e8ca142a8dfde9064febf498ca55d6ac098fe79444f605d732794","Name":"","Total":0,"Current":0,"Timestamp":"2023-08-10T18:00:22.336226751Z","Started":"2023-08-10T18:00:22.277476168Z","Completed":"2023-08-10T18:00:22.336226335Z"}],"done":true}
{"digest":"sha256:aac9b053bd01526a34fcda3b4c29b0e7563c8644ef47ecd67b8e908e08dec1f0","name":"[golatest 1/1] FROM docker.io/library/golang:1.20-alpine3.18@sha256:03278bc16e1a5b4fb6cdd3462108c060aa1e9c2353ce4d15d744b3c40168677d","statuses":[{"ID":"resolve docker.io/library/golang:1.20-alpine3.18@sha256:03278bc16e1a5b4fb6cdd3462108c060aa1e9c2353ce4d15d744b3c40168677d","Vertex":"sha256:aac9b053bd01526a34fcda3b4c29b0e7563c8644ef47ecd67b8e908e08dec1f0","Name":"","Total":0,"Current":0,"Timestamp":"2023-08-10T18:00:22.340789626Z","Started":"2023-08-10T18:00:22.291115668Z","Completed":"2023-08-10T18:00:22.340789168Z"}],"done":true}
{"digest":"sha256:8ad565762d44cf1653c371469b6e35b1fc41a74da2485b9e7c09673f7d5941b3","statuses":[{"ID":"transferring context:","Vertex":"sha256:8ad565762d44cf1653c371469b6e35b1fc41a74da2485b9e7c09673f7d5941b3","Name":"transferring","Total":0,"Current":755261,"Timestamp":"2023-08-10T18:00:22.705782626Z","Started":"2023-08-10T18:00:22.275040793Z","Completed":"2023-08-10T18:00:22.705782376Z"}],"done":true}
{"digest":"v4n5mbcrhrefgnjp2xpzelxcw","name":"[binaries-linux 4/4] COPY --link --from=buildkitd /usr/bin/buildkitd /","cached":true}

The basic gist of it is that each JSON blob has a digest attached to it and then the rest are only sections which correspond to updates on the vertex. The intervals are presently omitted but they can likely be attached to the done attribute or included as their own attribute if needed. It should be feasibly possible to use the JSON event stream to update a third-party UI with the information provided.

The name is included but only the first time that a vertex is printed. One of cached, done, or error will be printed when the vertex finishes. Statuses and logs are included as part of the blob.

I think this output will probably be best. The biggest concern for JSON outputs are repeated information (such as printing the digest over and over again) and redundant information (such as the full vertex struct). I also chose to keep partial log lines in as I think it's always possible for a third-party to choose to buffer partial log lines, but it's not possible to retrieve partial lines if the formatter isn't even giving them to you. Since the primary purpose of this output is to enable third-party progress bars, I thought it would be more simple to keep it and it can always be removed later without changing the external API.

This is only a mockup so I don't have code that I feel comfortable pushing for review yet.

@nicks
Copy link
Contributor

nicks commented Aug 9, 2023

so excited for this change, i've wanted something like this for years!!!

@jsternberg jsternberg marked this pull request as draft August 10, 2023 21:49
@jsternberg
Copy link
Collaborator Author

Converting this to a draft. I'm going to push an update to the code which contains the interfaces and structure that I think match with the review comments, but I still need to reorganize the code. I wanted to get something pushed before EOD though to get feedback.

@jsternberg jsternberg changed the title Adds a simple JSON output as an additional progress indicator progressui: adds a json output that shows raw events for the solver status Aug 10, 2023
Comment on lines 216 to 285
func DisplaySolveStatus(ctx context.Context, c console.Console, w io.Writer, ch chan *client.SolveStatus, opts ...DisplaySolveStatusOpt) ([]client.VertexWarning, error) {
var d SolveStatusDisplay
if c != nil {
d = NewConsoleDisplay(c)
} else {
d = newTextDisplay(w)
}
return UpdateDisplay(ctx, d, ch, opts...)
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can modify this if we want, but I think it will probably be better to keep this method around with its current signature until the places that use this have been updated. We can add a deprecation notice to it to ease that transition if it helps.

The new method is intended to be using NewDisplay to create the display and then UpdateDisplay to feed the display with the status updates.

@@ -164,7 +269,6 @@ type vertex struct {
logsOffset int
logsBuffer *ring.Ring // stores last logs to print them on error
prev *client.Vertex
events []string
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appeared to be dead code. While this variable was used, it never appeared to be set except in this package where it was cleared after it was read from.

Canceled bool `json:"canceled,omitempty"`
Error string `json:"error,omitempty"`
Cached bool `json:"cached,omitempty"`
Done bool `json:"done,omitempty"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of Done this should have the Start/Completed time.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modified this to just copy the Completed attribute from the client.SolveStatus instead of using Done.

printVtx(p *textMux, t *trace, v *vertex)
}

type vertexUpdate struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As these are the types returned to user that supposedly someone will parse make these types public (can also put definition in another package for easier import).

I don't think this is vertexUpdate. It is more like a invocation.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've adapted the various VertexX types into their own structs that match this package's API a bit more cleanly. They're mostly based on the ones in the client package, but with some small differences and the correct JSON annotations.

Even when they're alike, it's probably better to keep them separate just so changing one of these types doesn't inadvertently affect the output of the other.

I think the progressui package shouldn't be a problem for most people to import, but we can move the type with a type alias if it ends up being a problem in the future or consumers can just copy the file with the JSON definitions pretty easily into their own projects.

@jsternberg jsternberg force-pushed the json-printer branch 4 times, most recently from 35d9a5a to ce9cb90 Compare August 11, 2023 18:40
@jsternberg jsternberg marked this pull request as ready for review August 14, 2023 18:45
@jsternberg jsternberg requested a review from tonistiigi August 14, 2023 18:45
func DisplaySolveStatus(ctx context.Context, c console.Console, w io.Writer, ch chan *client.SolveStatus, opts ...DisplaySolveStatusOpt) ([]client.VertexWarning, error) {
modeConsole := c != nil
// SolveStatusDisplay is a display that can show the updates to the solver status.
type SolveStatusDisplay interface {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's avoid public interfaces with private methods. I think all-public is fine here unless we completely want to hide it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason why I decided to make this have private methods is because the methods themselves aren't really useful or implementable for someone outside of this package. If we want to expose this to allow a custom SolveStatusDisplay, I think we need to do a larger refactor to expose more of this package. I'm a bit uncomfortable with doing that without a clear use case.

I think, if there's a compelling need to expose this more, we can defer what this interface should look like until then. Doing it this way allows us to implement the feature without making a promise to anyone using the library.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm ok with NewDisplay just returning a struct and all this backend stuff being completely private.

//
// For more control over the display method, it is suggested to use NewDisplay and UpdateDisplay instead
// of this function.
func DisplaySolveStatus(ctx context.Context, c console.Console, w io.Writer, ch chan *client.SolveStatus, opts ...DisplaySolveStatusOpt) ([]client.VertexWarning, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is unused now. If correct then we can remove it and use d = NewDisplay(...); d.Show(ch). (I think it might make sense to make d and the backend interface for display different objects then).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can mark this function as deprecated. I don't think we should remove it in the same commit as adding a new method. It would still be used by other repositories that aren't connected to this one. Those should be given an opportunity to be able to migrate to the new API.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have backward compatibility guarantees for Go vendors. This will just add maintenance cost(and stops us from making changes if we need to). Importers will still need to update the code anyway. Marking function deprecated usually means nobody will remember to remove it.

// Information will only be included if it has changed since the last time
// that the information was sent or if it is the first time this vertex has
// been output.
type Vertex struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe everything but SolveStatus is already identical to the defined types. In that case, combine the types or use type aliases.

edit: I see the Vertex digest.Digest is different that could be added with a wrapper, embedding the shared object. For the VertexLog, the already defined type with Stream and without Partial is better.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's probably better to just keep them separate so we aren't tying the two APIs together. Yes, they'll share a lot of common attributes, but they're still different APIs. There is a bit of a difference here as you noted. This API kind of guarantees that the update struct will only contain data from one vertex rather than potentially having multiple different vertex updates in the same batch.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Otoh I don't want these to start to diverge. I think that is a bigger risk. We can move types to separate packages if the imports would become too big. This can be done later but mark it as follow-up after this PR merges then.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to just expose the raw solve status struct as-is? That might be better.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean the raw event structs as they appear from protobuf? In that case, it should be jsonraw. It's not as useful for users because the only way to understand it would be to convert it into structured form first (something that can't be done with jq for example).

)

// SolveStatus holds the latest status update for a vertex by its digest.
type SolveStatus struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe JSONProgress is clearer name.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe SolveStatusUpdate? This struct doesn't really communicate the JSON progress. It's just a struct that happens to have the metadata annotations to serialize to JSON with a certain format.

I do think it's unclear this is an update to the solve status and doesn't communicate the entirety of the process.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe VertexProgress as this object is scoped to a single vertex?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this name. I'll make that change.

@@ -28,13 +29,21 @@ type lastStatus struct {

type textMux struct {
w io.Writer
printer vertexPrinter
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, the refactoring of current progress objects and new interfaces would be in a separate commit from the new JSON display. But if you are unsure yet if this is the final version of the refactors we can delay that split.

current digest.Digest
last map[string]lastStatus
notFirst bool
nextIndex int
desc string
}

func newTextMux(w io.Writer, printer vertexPrinter) *textMux {
if printer == nil {
printer = (*textPrinter)(nil)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can imagine this construct being confusing for the readers. As this is all private, let's avoid it and ensure the caller always does the right thing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea sure. I'll change this.


type jsonPrinter struct {
seen map[digest.Digest]struct{}
buf SolveStatus
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite get the need for this buf property. If it just for passing data when calling methods from printVtx then it should be passed as function parameter.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is mostly to prevent large numbers of allocations for the slices in the SolveStatus struct. Since these slices are short-lived (used only for the marshaling), we don't need to be careful about the lifetime of the contents of the slice. So the printer keeps a slice around and truncates it to a size of zero but keeps its capacity the same to avoid extra allocations while printing.

It's just most simple to keep it here since this is the only section of code that uses this struct.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, ok. I'm not sure if this is premature optimization or not, but add a small comment explaining that this is for slice memory reuse.

Maybe it still makes sense to pass buf as a parameter to the other methods(turn them into helper functions).

if js.isEmpty() {
return
}
out, _ := json.Marshal(&js.buf)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

json.NewEncoder(w); enc.SetIndent("", " ");

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you expand on this? My intention was for the stream to come out unformatted and tools like jq or something else could perform the formatting if desired. Adding an intent will increase the amount of memory that this progress method utilizes.

I also may be wrong about this, but I believe there isn't a difference between json.NewEncoder(w).Encode(v) and json.Marshal(v) except the former has an extra memory allocation. The internal implementation of the encoder uses the same internal methods as marshal.

Looking at the source, there is a difference between the two when indenting is used, but MarshalIndent is likely more performant unless you store the json.Encoder struct.

See the source code in the standard library here: https://cs.opensource.google/go/go/+/refs/tags/go1.21.0:src/encoding/json/encode.go;l=179

In this one, it preallocates byte space for the indentation using a pretty simple metric. In contrast, here's the encoder variation: https://cs.opensource.google/go/go/+/refs/tags/go1.21.0:src/encoding/json/stream.go;l=224

This reuses a buffer that's kept in the encoder. But, this will only have allocated space if there was a previous encoding that happened. Since this struct is created, used, and immediately discarded, I believe it just increases memory usage.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

json.NewEncoder is optional. We already have a writer, so I think it is clearer to use the writer-based API rather than allocate output into a byte slice and write/format it into writer then. I don't think there is a memory allocation difference(as afaic atm Encode does not do partial writes for error handling), or at least that is not meaningful. The indent is because JSON format is also human readable, not only machine-readable, and we shouldn't make it harder for users to understand it. I don't think the marshaling speed is relevant here.

}

func newJSONDisplay(w io.Writer) SolveStatusDisplay {
return newTextDisplay(w, &jsonPrinter{})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to check this more but it looks to me that this newTextDisplay means that newTextMux is used for JSON as well. TextMux is the thing that tries print multiple parallel steps together in one text stream by making one of them active and then with control variables like antiFlicker/maxDelay etc controls how often the active item switches and when partial data is printed to avoid too much buffering. For JSON, I don't think this is ideal and we should print new JSON progress objects once the vertex goes to Complete state.

@jsternberg
Copy link
Collaborator Author

Went through and redid a bunch of this. The output is now named raw:json. If we want to do rawjson or just json that's fine, but I figured we'd follow up with a json output that will match more what you were originally talking about with batching the results and only printing when the vertex is complete. I thought raw:json would match better if we later went and did something like raw:proto or something.

This version just marshals it directly. I refactored the interface a bit and hid it within a struct.

I think this version should be more clean than the original.

Statuses []*VertexStatus
Logs []*VertexLog
Warnings []*VertexWarning
Vertexes []*Vertex `json:"vertexes,omitempty"`
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I should change this at the same time now that it will be publicly exposed, but the proper plural of vertex is vertices. That would involve a change to the field name though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I didn't realize this was a regional thing.

Copy link
Member

@tonistiigi tonistiigi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly just naming comments

@@ -46,7 +46,7 @@ var buildCommand = cli.Command{
},
cli.StringFlag{
Name: "progress",
Usage: "Set type of progress (auto, plain, tty). Use plain to show container output",
Usage: "Set type of progress (auto, plain, tty, raw:json). Use plain to show container output",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's avoid : as it implies a microformat(eg. tty:json). If you don't like rawjson other possibilities would be something from "events".

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My intention is that raw would be the format and json is the microformat for raw. Maybe it would be better to just label it raw and then add raw:json in the future if we ever decided to do other formats for the raw type.

I feel like events probably won't be descriptive enough.

if disp.phase == "" {
disp.phase = "Building"
}
type SolveStatusDisplay struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can just be Display


// NewPlainDisplay creates a new SolveStatusDisplay that outputs the status
// in a human-readable plain-text format.
func NewPlainDisplay(w io.Writer, opts ...DisplaySolveStatusOpt) SolveStatusDisplay {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leave these private until someone has a use-case. NewDisplay() should be enough.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll remove it. We had a section of code that manually used the plain display and it created this awkward code:

d, _ = progressui.NewDisplay(os.Stderr, progressui.PlainMode)

That's fine it just felt weird discarding the error. But, the error is needed for NewDisplay because tty mode can fail and the progress mode can be invalid.

// refresh updates the display with the latest state.
// This method only does something with displays that
// have buffered output.
refresh()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe flush() ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My intention behind this was to avoid coupling the intention with the implementation. I figured that the API for this was a bit similar to a double buffering API so update is used to update the internal state and then refresh is used to signal the display should be updated.

I can change it to flush but I felt the name refresh fit better for a display interface.

type solveStatusDisplay interface {
// init initializes the display and opens any resources
// that are required.
init(displayTimeout time.Duration)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think setting this to *rate.Limiter makes the intent clearer


func (d *rawJSONDisplay) done() {
// Flush if the underling writer supports that method.
if flusher, ok := d.w.(interface {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this should be handled here. If someone passes a buffered writer it is their responsibility to flush it after the display has completed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll remove this. My intention with this was to avoid any needed flushing for things like Stdout and Stderr and just to provide some convenience and parity to how the TTY output functions, but I don't think this is really necessary.

}

func (d SolveStatusDisplay) Warnings() []client.VertexWarning {
return nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 is this some leftover?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea this is a leftover. I was originally going to consolidate the warnings into the struct, but it didn't work and I went back to returning them from UpdateFrom. This just didn't get removed.

Copy link
Member

@tonistiigi tonistiigi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, squash the commits and make sure DCO is passing.

@jsternberg
Copy link
Collaborator Author

Squashed and ready to merge.

…tatus

This adds an additional display output for the progress indicator to
support a json output. It refators the progressui package a bit to add a
new method that takes in a `SolveStatusDisplay`. This
`SolveStatusDisplay` can be created by the user using `NewDisplay` with
the various modes as input parameters.

The json output will print the raw events as JSON blobs. It will not
throttle the messages or limit the display. It is meant as a pure raw
marshaling of the underlying event stream.

Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
@tonistiigi tonistiigi merged commit fa58a0b into moby:master Aug 25, 2023
@jsternberg jsternberg deleted the json-printer branch August 25, 2023 14:06
@ddl-retornam
Copy link

@jsternberg sorry to comment on an old PR but would it be possible to update this to export both newRawJSONDisplay and newPlainDisplay ? We have a tool that leans heavily on the io.Writer from the old func DisplaySolveStatus(ctx context.Context, c console.Console, w io.Writer, ch chan *client.SolveStatus, opts ...DisplaySolveStatusOpt) which NewDisplay no longer provides. Thank you

@tonistiigi
Copy link
Member

@ddl-retornam With NewDisplay you can create instance of any display type by passing the correct mode parameter.

@ddl-retornam
Copy link

@ddl-retornam With NewDisplay you can create instance of any display type by passing the correct mode parameter.

@tonistiigi NewDisplay with the mode accepts console.File whilst newPlainDisplay accepts io.Writer am I missing something here?

@tonistiigi
Copy link
Member

Ah, yes, that is not correct. It should just accept io.Writer.

@ddl-retornam
Copy link

ddl-retornam commented Sep 6, 2023

Ah, yes, that is not correct. It should just accept io.Writer.

@tonistiigi from a cursory glance it might be best to have it accept both console.File and io.Writer. Please advise on the best way forward.

@jsternberg
Copy link
Collaborator Author

I think modifying the interface to take in an io.Writer is fine. My original thought was to use an io.Writer but I decided to use the more specific interface to allow for typing to be a bit more explicit. But io.Writer is a valid output sink. I don't think we need an API with both console.File and io.Writer. If there ends up being some benefit to having an API that takes console.File, we can add a new function like NewFileDisplay. I don't think we should do that yet though as there's not much of a benefit other than typing assistance.

@tonistiigi
Copy link
Member

@jsternberg There is already a runtime variable type detection for tty, even if we pass console.File, so I don't think there is an issue with passing io.Writer and just modifying that detection check a bit.

kzys added a commit to superfly/flyctl that referenced this pull request Mar 29, 2024
DisplaySolveStatus has been removed by
moby/buildkit#4113.
kzys added a commit to superfly/flyctl that referenced this pull request Mar 29, 2024
DisplaySolveStatus has been removed by
moby/buildkit#4113.
kzys added a commit to superfly/flyctl that referenced this pull request Mar 29, 2024
DisplaySolveStatus has been removed by
moby/buildkit#4113.
kzys added a commit to superfly/flyctl that referenced this pull request Apr 5, 2024
* refactor: don't use WithFailFast

It is no-op and has been removed from Buildkit.

moby/buildkit#4484

* chore: upgrade moby/buildkit

DisplaySolveStatus has been removed by
moby/buildkit#4113.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants