Notes › Tech Notes › How a 1 GB File Is Built From 1024 Chunks: Chunk Rhythm, Cancel Boundaries and Three Fill Bytes in a File Generator

How a 1 GB File Is Built From 1024 Chunks: Chunk Rhythm, Cancel Boundaries and Three Fill Bytes in a File Generator

A 1 GB file is stitched from 1024 chunks of 1048576 bytes; cancel only flips one flag and stops at a chunk boundary. The progress area keeps 44% and 454.00 MB, while the three fill modes differ in byte trimming and shown values.

I clicked cancel and the progress area froze at 44%

A colleague wanted a 1 GB file for a local read-write case, so I built one in the browser. I typed 1 GB and pressed generate; when progress hit 44% I worried the page would not hold up, so I hit cancel. The button became clickable on the spot, yet the progress area stayed pinned where it was: 44%, 454.00 MB and elapsed 0.7s, not one character cleared. Why did it stop at 454, a number that lands between two stools? The answer sits at a chunk boundary.

A file is saved chunk by chunk, then packed once at the end

Generation is one while loop: the source hard-codes var chunkSize = 1024 * 1024, so each round produces a 1048576-byte Uint8Array pushed into a local array, and the last round is trimmed by Math.min(chunkSize, remaining); only once the array holds every chunk does the code call new Blob, and the label is rewritten to 正在打包下载.... The chunk count is the target bytes divided by 1048576:

TargetChunksLast-chunk bytes
3 B13
1.5 MB2524288
10 MB101048576
500 MB5001048576
1 GB1024divides evenly, no short chunk

1073741824 / 1048576 = 1024, so 1 GB is stitched together from 1024 equal-length chunks with no trimming round at all. The browser took 1449ms for a full run and the panel settled on 1.45s, and the Blob received exactly 1024 parts; on the same machine the Node version wrote each chunk to disk in 1494ms, and fs.statSync reported 1073741824 bytes. The fill mode also changes the pace: in the any size file generator panel, 100 MB of random data took 186ms while all zeros took 133ms, and that 53ms is exactly the cost of the random branch filling each 65536-byte segment.

Cancel is one assignment, and the stop lands on a chunk boundary

The cancelGenerate function body is a single faCancelled = true line. The checkpoint sits inside the loop before every chunk begins; when it is hit the tool pops 已取消生成 and then breaks. Cancel is not a foot brake, it is "do not produce the next chunk", so the longest delay is one chunk of generation time plus one 15ms yield. In the 1 GB sample the cancel was pressed at 721ms and it stopped on chunk 454, at 44%, which works out to about 1.59ms per chunk; in that same run the Blob constructor, createObjectURL and a.click all counted 0, because the cancel path never reached the packing stage, and "where did the already-generated blob go" has no object to answer it: those 454 chunks hung on a local array and left scope once the function returned.

1 GB chunk pace and cancel point 44% 721ms cancel, stop at chunk 454 each chunk 1048576 bytes 1024 chunks total, only 454 ran about 1.59ms per chunk Blob built 0 times

Figure 1: the chunk boundary and the on-screen residue left by the 1 GB cancel sample

Reset covers only the two buttons

resetUI does only two things: re-enable the generate button and hide the cancel button. The progress area is not on that list, so 44%, 454.00 MB and elapsed 0.7s stay in place, and the result panel is still display:none. faCancelled is not cleared by itself either; it is reset by the first line of the next generate run. I ran a 10 MB job afterwards to check for contamination: the toast showed only 文件已生成并开始下载, and the panel printed 文件名:testfile.bin大小:10.00 MB (10,485,760 字节)耗时:0.02s模式:随机数据 as normal. What lingers is the display, not the state.

Each chunk writes 3 text nodes, but the progress bar changes only 45 times

During those 454 chunks the percentage, the generated amount and the elapsed time each had their text rewritten 454 times, yet the progress bar's style attribute changed only 45 times: after rounding, the percentage holds the same value for about 10 chunks in a row, and re-assigning an unchanged value is not counted as a change. The 10 MB round produced 11 changes, being 10 loop chunks plus one closing 100%. Elapsed time runs two precisions as well: the progress area keeps one decimal place (0.7s), the settled result keeps 2 (0.02s and 1.45s).

What the three fill modes lay down into the bytes

The all-zero branch returns a pre-cleared chunk straight away, so the first and last 16 bytes are all 00. The random branch uses Math.min(65536, size - off) and fills a whole chunk in 16 passes; inside that same chunk 0x00 appears 4078 times, which lines up with the 4096-times uniform expectation computed from 256 possible values per byte. The custom branch is TextEncoder output overwritten in a cycle: 你好abc is 9 bytes in UTF-8, e4 bd a0 e5 a5 bd 61 62 63, and laid into a 20-byte chunk it gives two full cycles followed by e4 bd; AB poured into a 5-byte chunk yields 41 42 41 42 41, that is ABABA. Empty text would degrade to all zeros, but a fourth guard, 请输入自定义填充文本, blocks it before the UI.

product bytes of the three modes all-zero head and tail 16 00 00 00 00 00 00 random 65536 filled 16 times 00 counted 4078, expected 4096 custom 9 into a 20-byte chunk e4 bd a0 e5 a5 bd 61 62 63 e4 bd tail cut AB into a 5-byte chunk 41 42 41 42 41 half cycle cut

Figure 2: measured bytes and cycle trimming for the three fill modes

Shown values differ from real bytes in two places

One place is the first screen: the static initial value of fa-size-val is written as 1.00 MB (1048576 字节), and the moment the first updateSizeInfo() runs it is overwritten by the thousand-separated 1.00 MB (1,048,576 字节). The other place is the color change: the red text compares the true byte count (it only writes #ff6666 when bytes > 500 * 1048576), yet the shown value rounds first, so 524288001 bytes is displayed as "500.00 MB" while the color is already warning red. The progress area only offers humanReadable, without the exact byte count.

Four guards and one hard ceiling

At the top of generate sit four guards. Zero, a negative number, an empty string and a non-number all follow the same path and return 0 inside getTotalBytes, and the UI shows only one message: 请输入有效的文件大小. The over-1 GB guard runs before the confirm dialog: enter 1024 GB (1073741824000 bytes) and only 文件过大,请勿超过 1 GB pops up, the confirm dialog never opening once. Exactly 1 GB is legal because the comparison is strict greater-than, yet it still has to pass a 500 MB confirm; 524288000 bytes is also strictly greater, but it generates in silence. That 600 MB run I clicked No: the toast stayed empty, the Blob constructor ran 0 times, and the button never even entered disabled.

The download link lives for only 1000 milliseconds

The filename is taken from value.trim() || "testfile.bin", which strips only the outer spaces: entering " mydata .dat " gives a download attribute of mydata .dat, and noext does not append an extension. On the success path the measured gap from createObjectURL to revokeObjectURL is 1000ms, and under a file:// origin the prefix looks like blob:null/; a second later it is already reclaimed.

FAQ

Q: Can those numbers left in the progress area after a cancel still be trusted?

They are the true readings at the moment of cancel: 454.00 MB and 44% match the 454 chunks already produced, they simply stop updating, because resetUI never covered the progress area.

Q: Why does 1 GB have no last chunk at all?

1024 divides evenly with no remainder, so Math.min(chunkSize, remaining) always yields a full chunk at that size; only switching to 1.5 MB trims out a short chunk of 524288.

Q: Does clicking No on the 500 MB confirm dialog count as a failure?

No, it did not even change the UI state. As for how large to enter this time, the same tool has an older article, making a test file of a chosen size on demand.

📎 Related articles

Three Edges of Filename Extraction: The Trailing Slash Backstops One Step, the Query String Stays Whole, Blank Lines Vanish Silently 2026-11-04 Turn encodeAll On and Chinese First Becomes 中: HTML Entity Table Coverage and Decode Tolerance, Measured 2026-11-04 HTML Beautify and Minify Are Not Inverses: A Round-Trip Byte Audit of 10 Samples 2026-11-04 You Paste 100 Fraction Rows and Row 45 Quietly Becomes 1/4: Who Owns the Minus Sign, Blank Lines and Big Numbers on This Page 2026-11-03 Paste 3. Start Into Markdown to HTML: Why Ordered List Numbering Only Honours the First Number 2026-11-03