Improved details for build and pull#40596
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds richer progress reporting to wslc build and wslc pull: pull now shows progress bars with current/total bytes, and build shows per-layer download progress lines that update in place along with colored build-step output.
Changes:
- Plumb
current/totalbyte counts from BuildKit status throughBuildKitStatusJSON,reportProgress, and into the callbacks. - Render a fixed-width progress bar with
FormatBytesoutput inImageProgressCallback, and truncate to console width to avoid wrap artifacts. - In
BuildImageCallback, maintain a per-entrym_pullLinesmap rendered each redraw, and colorize permanent build-step lines in teal.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/windows/wslcsession/WSLCSession.cpp | Extends reportProgress to forward current/total and emits per-entry progress when entry.total > 0. |
| src/windows/wslc/services/ImageProgressCallback.cpp | Replaces percent text with a 30-char progress bar plus formatted byte counts; truncates to console width. |
| src/windows/wslc/services/BuildImageCallback.h | Adds m_pullLines map and <map> include. |
| src/windows/wslc/services/BuildImageCallback.cpp | Stores per-id pull status, redraws periodically, and wraps permanent lines in teal ANSI escapes. |
| src/windows/inc/docker_schema.h | Adds current/total fields to BuildKitStatus deserialization. |
| src/shared/inc/stringshared.h | New FormatBytes helper producing GB/MB/KB/B strings. |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
| // accepting any non-empty id, so future or unrelated id usage defaults to permanent. | ||
| const bool isLog = (id != nullptr && std::string_view{id} == "log"); | ||
|
|
||
| // Pull/download progress: update the per-entry map so Redraw can show each entry |
There was a problem hiding this comment.
I'm not sure we are handling this correctly when m_isConsole is false. We should probably skip the whole callback stuff when non in an interactive terminal.
There was a problem hiding this comment.
I think this would break AIs who are scraping wslc.exe build output. Right now we output something like this:
PS C:\cDev\copilot\build-test> cat .\dockerfilesimple.log
Building image from directory: .
[1/2] FROM docker.io/library/ubuntu:24.04@sha256:c4a8d5503dfb2a3eb8ab5f807da5bc69a85730fb49b5cfca2330194ebcc41c7b
[2/2] RUN echo 123
exporting to image
| exporting layers
| writing image sha256:780c43a5ab95735b080898e63db96c3feebdd05591a0a934bc3252fe70201004
| naming to docker.io/library/test
PS C:\cDev\copilot\build-test>
I think this is pretty reasonable and gives the right info on all the steps.
…m/microsoft/WSL into user/crloewen/better-build-output
| inline std::wstring FormatBytes(uint64_t bytes) | ||
| { |
There was a problem hiding this comment.
I am wondering if we could move this to wsl::windows::common::string and use the windows StrFormatByteSizeEx. It does have a slightly different oputput. StrFormatByteSizeEx uses 1024-based math with the Windows-conventional labels (KB, MB, GB, TB) — i.e. 1.00 KB means 1024 bytes. The current helper is 1000-based. So switching will change the numbers users see during pulls which would diverge from docker's behavior. However, it does give localized decimal seperator. I could see this going either way.
ALSO, if we are making a helper for this, we should probably get rid of the other FormatBytes method in ContainerTasks.cpp:37 and just use a common helper.
There was a problem hiding this comment.
Opportunity to centralize this, such as moving the one from ContainerTasks into stringshared.h
I'm not sure if Craig's PR is the right place to do that consolidation, but at the very least using the same method for it is appropriate.
| while (!wide.empty() && (wide.back() == L'\n' || wide.back() == L'\r')) | ||
| { | ||
| terminator.insert(terminator.begin(), wide.back()); | ||
| wide.pop_back(); | ||
| } |
There was a problem hiding this comment.
This is a bit hard to reason about. I think this can be simplified with std::wstring::find_last_not_of(L"\r\n").
const auto bodyLength = wide.find_last_not_of(L"\r\n") + 1; // or textLength or whatever you prefer (I am bad at naming stuff)
const auto newlines = wide.substr(bodyLength);
wide.resize(bodyLength);
WriteTerminal(std::format(L"\033[36m{}\033[0m{}", wide, newlines));
| const bool isLog = (id != nullptr && std::string_view{id} == "log"); | ||
|
|
||
| // Pull/download progress: update the per-entry map so Redraw can show each entry | ||
| // on a single line that updates in place. |
There was a problem hiding this comment.
Nit: The OnProgress body asks the same three questions in two different places — "does the message have an id?", "is it the log sentinel?", "is it per-layer pull progress?" — and the id != nullptr && *id != '\0' check appears several times. The verbose/non-TTY branch and the interactive branch also independently re-derive what kind of message this is.
const std::string_view idView = (id != nullptr) ? id : std::string_view{};
const bool isLog = (idView == "log");
const bool isPullProgress = (!idView.empty() && total > 0 && !isLog);
if (m_verbose || !m_isConsole)
{
if (!isPullProgress)
{
wprintf(L"%hs", status);
}
return S_OK;
}
if (isPullProgress) { return HandlePullProgress(idStr, status); }
if (isLog) { return HandleLogChunk(status); }
return HandleStepHeader(status);
| terminator.insert(terminator.begin(), wide.back()); | ||
| wide.pop_back(); | ||
| } | ||
| wide = std::format(L"\033[36m{}\033[0m{}", wide, terminator); |
There was a problem hiding this comment.
Really nit, don't hate me for this. There are a lot of these ANSI escape sequences scattered throught the file, it would be nice to have to have these defined as constants for readability. Either at the top of this file or a common header. (IDK if its used elsewhere).
For example, this format could read like: wide = std::format(L"{}{}{}{}", ansi::Cyan, wide, ansi::ResetColor, terminator);
Then its a lot easier to understand what is happening.
There was a problem hiding this comment.
Actually, I did a brief search with copilot, and alledgedly its also used in WSLCTests.cpp, WSLCE2EHelpers.h, ImageProgressCallback.cpp, BuildImageCallback.cpp. So this could warrant placing in stringshared.h or deps.h. The test files i mentioned use the VT_* constants.
There was a problem hiding this comment.
I was about to make the same comment. I'm not sure this PR should do a lot of refactoring, which could make it a lot bigger than it should be, but at the very least define the VT sequences as constants so the construction is a lot easier to read and so we can do that refactoring more easily later.
| // Per-entry pull progress lines, keyed by entry id. Updated in place by Redraw. | ||
| std::map<std::string, std::string> m_pullLines; |
There was a problem hiding this comment.
The comment doesn't capture the load-bearing detail here: std::map's ordered iteration is what makes the visual "in place" rendering work. Redraw walks the map in key order each frame, so each layer id consistently lands on the same row — swap this for unordered_map and entries would shuffle between frames. Worth calling that out.
| std::wstring bar(c_progressBarWidth, L' '); | ||
| for (int i = 0; i < filled; ++i) | ||
| { | ||
| bar[i] = L'='; | ||
| } | ||
|
|
||
| if (filled < c_progressBarWidth) | ||
| { | ||
| bar[filled] = L'>'; | ||
| } |
There was a problem hiding this comment.
This pre-fills with spaces and then overwrites characters by index which is fine, but std::wstring already has constructors and append(n, ch) that express this directly without an explicit loop or index arithmetic. The intent reads more clearly when the bar is constructed segment-by-segment:
std::wstring bar;
bar.reserve(c_progressBarWidth);
bar.append(filled, L'=');
bar.append(L">");
bar.resize(c_progressBarWidth, L' ');
There was a problem hiding this comment.
Recommend to use a std::wostringstream for these constructions rather than doing repeated manipulation of the same string. The stringstreams were designed to do that efficiently. Then convert once to a wstring when all manipulations are complete.
| while (line.size() < static_cast<size_t>(visibleWidth)) | ||
| { | ||
| line += L' '; | ||
| } |
There was a problem hiding this comment.
nit: line.resize(visibleWidth, L' '); - no need for while loop
dkbennett
left a comment
There was a problem hiding this comment.
Echo a lot of the great feedback Kevin had. I like that this PR is relatively small though, and if refactoring things would lead to it growing significantly I would recommend not doing that and having a separate PR for that refactoring. But definitely all for things that make the refactoring easier at the very least.
| inline std::wstring FormatBytes(uint64_t bytes) | ||
| { |
There was a problem hiding this comment.
Opportunity to centralize this, such as moving the one from ContainerTasks into stringshared.h
I'm not sure if Craig's PR is the right place to do that consolidation, but at the very least using the same method for it is appropriate.
| terminator.insert(terminator.begin(), wide.back()); | ||
| wide.pop_back(); | ||
| } | ||
| wide = std::format(L"\033[36m{}\033[0m{}", wide, terminator); |
There was a problem hiding this comment.
I was about to make the same comment. I'm not sure this PR should do a lot of refactoring, which could make it a lot bigger than it should be, but at the very least define the VT sequences as constants so the construction is a lot easier to read and so we can do that refactoring more easily later.
kvega005
left a comment
There was a problem hiding this comment.
On a successful build, the dim scrolling window gets erased by the final CollapseWindow(), so once the build finishes only the teal step headers ([1/4] FROM …, [2/4] RUN …) remain in scrollback. The replay branch in the destructor only fires when std::uncaught_exceptions() indicates failure.
For comparison, DOCKER_BUILDKIT=0 docker build keeps all RUN stdout in scrollback interleaved with step headers. So things like npm WARN …, pip resolver notes, or a BUILD SUCCESSFUL in 47s line from a tool inside the container aren't recoverable after a successful wslc build without re-running it.
Not sure if that's intentional (the compact view is genuinely nice) — flagging in case it isn't. A couple of options if you do want to address it:
- Drop the
std::uncaught_exceptions()check so the log replays on success too.m_allLinesis already populated and bounded byc_maxAllLinesBytes. - Or add a
--progress=plain/--quiet-style opt-out (could just reuse the existingm_verbosepath).
Summary of the Pull Request
Added some small details to
wslc buildandwslc pull.Specifically this just adds a 'total MB' to build and progress bars and total layer sizes to pull.
Pull
Before:

After:

Build
Before:

After:

PR Checklist
Detailed Description of the Pull Request / Additional comments
Scoped it to be as small as possible.
Validation Steps Performed
Tested behaviour before and after.