using Amphora; using Annulus; using Azimuth; using Clave; using Glass.Editing; using System; using System.Diagnostics; using System.Globalization; using System.Text; namespace Glass.Testbed.TestCases.Text { /// /// A TextBox over a text of a billion chars that exists only between Generate and Clear. It is built in memory a /// chunk per frame, so nothing is stored on disk and the window stays alive while it fills. /// public class HugeTextTestCase : TestCase { private const int TargetChars = 1000000000; private const int ChunkChars = 1000000; private const int ProgressEveryChunks = 8; private const int BoxWidth = 600; private const int BoxLines = 16; // A chunk ends on the first word past ChunkChars, which no word and separator together can stretch past this. private const int ChunkSlack = 64; private static readonly string[] Words = { "the", "quick", "brown", "fox", "jumps", "over", "lazy", "dog", "and", "then", "keeps", "running", "through", "tall", "grass", "toward", "a", "river", "that", "bends", "around", "old", "stone", "bridge", "where", "children", "sometimes", "gather", "to", "watch", "boats", "drift", "slowly", "downstream", "carrying", "nothing", "but", "light", "reflected", "from", "windows", "of", "houses", "built", "centuries", "before", "anyone", "thought", "of", "counting", "characters", }; [Flags] private enum CopyRefusedAnswer { Ok = 1, } private static readonly TextMaterialStyle ButtonTextStyle = Widgets.MenuLabelStyle with { HorizontalAlignment = Alignment.Horizontal.Center }; private readonly StringBuilder chunkText = new StringBuilder(ChunkChars + ChunkSlack); private readonly char[] chunkChars = new char[ChunkChars + ChunkSlack]; private readonly Stopwatch generationWatch = new Stopwatch(); private TextBox box; private MessageBox copyRefusedBox; private Label readout; private bool generating; private bool wholeLines; private bool awaitingCount; private int chunksMade; private static long MegabytesHeld() { return GC.GetTotalMemory(forceFullCollection: false) / 1048576; } private void Show(string text) { readout.Text = text; } private string Describe(string what) { return string.Format(CultureInfo.InvariantCulture, "{0:N0} chars {1}, {2:N0} MB held", box.Buffer.Length, what, MegabytesHeld()); } private void Clear() { generating = false; awaitingCount = false; box.Buffer.Replace(0, box.Buffer.Length, ""); GC.Collect(); Show("Empty. " + Describe("in the box")); } private void Start(bool inLines) { Clear(); wholeLines = inLines; chunksMade = 0; generating = true; generationWatch.Restart(); } // Words picked by position alone, so the same press always builds the same text. private void BuildChunk(int number) { chunkText.Clear(); chunkText.Append("[chunk ").Append(number.ToString(CultureInfo.InvariantCulture)).Append("] "); int word = number * 7919; while (chunkText.Length < ChunkChars) { chunkText.Append(Words[(word * 31 + word / 7) % Words.Length]); if (wholeLines && word % 120 == 119) { chunkText.Append(".\n\n"); } else if (wholeLines && word % 12 == 11) { chunkText.Append(".\n"); } else if (word % 12 == 11) { chunkText.Append(". "); } else { chunkText.Append(' '); } ++word; } } // The caret stays where it was, because an insert would carry it to the end and the next frame would follow it // there, counting every line in one go. private void AppendChunk() { BuildChunk(chunksMade); chunkText.CopyTo(0, chunkChars, 0, chunkText.Length); int caret = box.Buffer.Caret; box.Buffer.Replace(box.Buffer.Length, 0, new StringView(chunkChars, 0, chunkText.Length)); box.Buffer.MoveTo(caret, extend: false); ++chunksMade; } // The box only says a copy was refused. What to do about it is the application's, which here is a message. private void CopyRefused(TextBox refusedBox, int length) { copyRefusedBox.Message = string.Format(CultureInfo.InvariantCulture, "The clipboard couldn't take {0:N0} chars. Nothing was copied or cut.", length); copyRefusedBox.Open(); } private void AddButton(ref Rect row, string text, Action pressed) { Vector size = Widgets.ButtonLabel(text).PreferredSize(Constraints.Unbounded); Button button = new Label(Widgets.ButtonLabelStyle, text).FillParent( new Button(ButtonBehavior.Default) { DebugName = text, ButtonMaterial = UiPacksheetMaterials.Button, Bounds = row.NewCol(size.X, margin: Margin), }); button.OnActivate += delegate (ButtonBase _, in KeyboardSnapshot __, KeyboardManager ___) { pressed(); }; Add(button); } protected override void Populate() { AddCaption("Builds a text of a billion chars in memory, a million a frame. Nothing is written to disk."); AddCaption("The text lives until Clear, or until you leave this case. It takes about 2.6 GB."); AddCaption("The caret stays at the start, so lines are counted a frame at a time as it fills."); AddCaption("A jump or drag far into uncounted text counts the gap in one go."); AddCaption("The box keeps no undo history, so editing it never copies the text."); AddCaption("Ctrl+A then Ctrl+C copies the text into the clipboard a piece at a time, needing 2 GB for it."); AddCaption("Ctrl+V reads the clipboard a piece at a time, and moves what it read in as whole pages."); AddCaption("A copy the clipboard refuses opens a message box, through the box's OnCopyRefused."); readout = new Label(Widgets.CaptionStyle, "Empty."); readout.Bounds = ContentAggregator.NewRow(readout.PreferredSize(Constraints.Unbounded).Y); Add(readout); Scalar buttonHeight = Widgets.ButtonLabel("Generate").PreferredSize(Constraints.Unbounded).Y; Rect buttons = ContentAggregator.NewRow(buttonHeight); AddButton(ref buttons, "Generate one line", () => Start(inLines: false)); AddButton(ref buttons, "Generate many lines", () => Start(inLines: true)); AddButton(ref buttons, "Clear", Clear); box = new TextBox(new TextBoxBehavior { Multiline = true }) { DebugName = "huge text", TextBoxMaterial = Widgets.TextBox, }; Scalar barWidth = Widgets.TextBox.ScrollBar.Value.PreferredThickness(Orientation.Vertical); Scalar boxHeight = Widgets.FieldTextStyle.SingleLineHeight * BoxLines; box.Bounds = Rect.FromPositionAndSize(Vector.Zero, Widgets.TextBox.Background.BoundsSizeFor(new Vector(BoxWidth + barWidth, boxHeight))); box.Buffer.UndoBudget = 0; Rect row = ContentAggregator.NewRow(box.Bounds.Height); box.Bounds = box.Bounds.WithPosition(row.NewCol(box.Bounds.Width).TopLeft); Add(box); copyRefusedBox = new MessageBox(CopyRefusedAnswer.Ok, DialogBehavior.Default) { DefaultAnswer = CopyRefusedAnswer.Ok, CancelAnswer = CopyRefusedAnswer.Ok, MessageBoxMaterial = new MessageBoxMaterial { Background = UiPacksheetMaterials.Panel, TextStyle = Widgets.MenuLabelStyle, Spacing = Margin, } .WithButton(CopyRefusedAnswer.Ok, UiPacksheetMaterials.Button.Framing( new TextMaterial { Text = "OK", Style = ButtonTextStyle })), }; Add(copyRefusedBox); box.OnCopyRefused += CopyRefused; } // The text is the biggest thing the testbed holds, so it goes when the case is left. protected override void Teardown() { Clear(); } public override void Update(Duration elapsed) { if (generating) { AppendChunk(); if (box.Buffer.Length >= TargetChars) { generating = false; awaitingCount = true; Show(Describe(string.Format(CultureInfo.InvariantCulture, "built in {0:F1} s, counting lines", generationWatch.Elapsed.TotalSeconds))); } else if (chunksMade % ProgressEveryChunks == 0) { Show(Describe("built so far")); } } else if (awaitingCount && box.AllLinesCounted) { awaitingCount = false; Show(Describe("built, and every line is counted")); } } } }