From 54d559ad500d2b558264d45750675abb5b390698 Mon Sep 17 00:00:00 2001 From: Andy McFadden Date: Tue, 2 Jul 2024 14:49:17 -0700 Subject: [PATCH] Add formatted MLC cache Multi-line comments (MLCs) can span multiple lines, and are formatted with word-wrapping. This isn't too expensive now, but languages with immutable strings aren't ideal for this sort of thing. Before we introduce fancier formatting, we want to ensure that we're not going to adversely affect rendering performance. The cache entry for a given offset is tied to the MLC object and the Formatter; if either are changed, the cached string list will not be used. --- SourceGen/FormattedMlcCache.cs | 117 +++++++++++++++++++++++++++++ SourceGen/FormattedOperandCache.cs | 67 +++++++++++------ SourceGen/LineListGen.cs | 34 +++++++-- SourceGen/SourceGen.csproj | 1 + 4 files changed, 192 insertions(+), 27 deletions(-) create mode 100644 SourceGen/FormattedMlcCache.cs diff --git a/SourceGen/FormattedMlcCache.cs b/SourceGen/FormattedMlcCache.cs new file mode 100644 index 0000000..f4014e5 --- /dev/null +++ b/SourceGen/FormattedMlcCache.cs @@ -0,0 +1,117 @@ +/* + * Copyright 2024 faddenSoft + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +using Asm65; + +namespace SourceGen { + /// + /// Holds a cache of formatted multi-line comments. + /// + /// + /// We need to discard the entry if the MLC changes or the formatting parameters + /// change. MLCs are immutable and Formatters can't be reconfigured, so we can just do + /// a quick reference equality check. + /// + public class FormattedMlcCache { + /// + /// One entry in the cache. + /// + private class FormattedStringEntry { + public List Lines { get; private set; } + + private MultiLineComment mMlc; + private Formatter mFormatter; + + public FormattedStringEntry(List lines, MultiLineComment mlc, + Formatter formatter) { + // Can't be sure the list won't change, so duplicate it. + Lines = new List(lines.Count); + foreach (string str in lines) { + Lines.Add(str); + } + + mMlc = mlc; + mFormatter = formatter; + } + + /// + /// Checks the entry's dependencies. + /// + /// True if the dependencies match. + public bool CheckDeps(MultiLineComment mlc, Formatter formatter) { + bool ok = (ReferenceEquals(mMlc, mlc) && ReferenceEquals(mFormatter, formatter)); + return ok; + } + } + + /// + /// Cached entries, keyed by file offset. + /// + private Dictionary mStringEntries = + new Dictionary(); + + + /// + /// Retrieves the formatted string data for the specified offset. + /// + /// File offset. + /// Formatter dependency. + /// A reference to the string list, or null if the entry is absent or invalid. + /// The caller must not modify the list. + public List GetStringEntry(int offset, MultiLineComment mlc, Formatter formatter) { + if (!mStringEntries.TryGetValue(offset, out FormattedStringEntry entry)) { + DebugNotFoundCount++; + return null; + } + if (!entry.CheckDeps(mlc, formatter)) { + //Debug.WriteLine(" stale entry at +" + offset.ToString("x6")); + DebugFoundStaleCount++; + return null; + } + DebugFoundValidCount++; + return entry.Lines; + } + + /// + /// Sets the string data entry for the specified offset. + /// + /// File offset. + /// String data. + /// Multi-line comment to be formatted. + /// Formatter dependency. + public void SetStringEntry(int offset, List lines, MultiLineComment mlc, + Formatter formatter) { + Debug.Assert(lines != null); + FormattedStringEntry fse = new FormattedStringEntry(lines, mlc, formatter); + mStringEntries[offset] = fse; + } + + // Some counters for evaluating efficacy. + public int DebugFoundValidCount { get; private set; } + public int DebugFoundStaleCount { get; private set; } + public int DebugNotFoundCount { get; private set; } + public void DebugResetCounters() { + DebugFoundValidCount = DebugFoundStaleCount = DebugNotFoundCount = 0; + } + public void DebugLogCounters() { + Debug.WriteLine("MLC cache: valid=" + DebugFoundValidCount + ", stale=" + + DebugFoundStaleCount + ", missing=" + DebugNotFoundCount); + } + } +} diff --git a/SourceGen/FormattedOperandCache.cs b/SourceGen/FormattedOperandCache.cs index 13abc6b..50fb3f2 100644 --- a/SourceGen/FormattedOperandCache.cs +++ b/SourceGen/FormattedOperandCache.cs @@ -21,31 +21,38 @@ using Asm65; namespace SourceGen { /// - /// Holds a cache of formatted lines. + /// Holds a cache of formatted operands that may span multiple lines. /// /// - /// This is intended for multi-line items with lengths that are non-trivial to compute, - /// such as long comments (which have to do word wrapping) and strings (which may - /// be a mix of characters and hex data). The on-demand line formatter needs to be able - /// to render the Nth line of a multi-line string, and will potentially be very - /// inefficient if it has to render lies 0 through N-1 as well. (Imagine the list is - /// rendered from end to start...) Single-line items, and multi-line items that are - /// easy to generate at an arbitrary offset (dense hex), aren't stored here. + /// This is intended for multi-line items with line counts that are non-trivial to + /// compute, such as strings which may be a mix of characters and hex data. The on-demand + /// line formatter needs to be able to render the Nth line of a multi-line operand, and will + /// potentially be very inefficient if it has to render lines 0 through N-1 as well. (Imagine + /// the list is rendered from end to start...) Single-line items, and multi-line items that + /// are easy to generate at an arbitrary offset (dense hex), aren't stored here. /// - /// The trick is knowing when the cached data must be invalidated. For example, a - /// fully formatted string line must be invalidated if: - /// - The Formatter changes (different delimiter definition) - /// - The FormatDescriptor changes (different length, different text encoding, different - /// type of string) - /// - The PseudoOpNames table changes (potentially altering the pseudo-op string used) + /// The trick is knowing when the cached data must be invalidated. For example, a + /// fully formatted string line must be invalidated if: + /// + /// The Formatter changes (different delimiter definition) + /// The FormatDescriptor changes (different length, different text encoding, different + /// type of string) + /// The PseudoOpNames table changes (potentially altering the pseudo-op + /// string used) + /// /// - /// Doing a full .equals() on the various items would reduce performance, so we use a + /// Doing a full .equals() on the various items would reduce performance, so we use a /// simple test on reference equality when possible, and expect that the client will try - /// to ensure that the various bits that are depended upon don't get replaced unnecessarily. + /// to ensure that the various bits that are depended upon don't get replaced + /// unnecessarily. + /// We don't make much of an effort to purge stale entries, since that can only happen + /// when the operand at a specific offset changes to something that doesn't require fancy + /// formatting. The total memory required for all entries is relatively small. /// public class FormattedOperandCache { - private const bool VERBOSE = false; - + /// + /// One entry in the cache. + /// private class FormattedStringEntry { public List Lines { get; private set; } public string PseudoOpcode { get; private set; } @@ -91,6 +98,9 @@ namespace SourceGen { } } + /// + /// Cached entries, keyed by file offset. + /// private Dictionary mStringEntries = new Dictionary(); @@ -102,20 +112,23 @@ namespace SourceGen { /// Formatter dependency. /// FormatDescriptor dependency. /// PseudoOpNames dependency. - /// Pseudo-op for this string. - /// A reference to the string list. The caller must not modify the - /// list. + /// Result: pseudo-op for this string. + /// A reference to the string list, or null if the entry is absent or invalid. + /// The caller must not modify the list. public List GetStringEntry(int offset, Formatter formatter, FormatDescriptor formatDescriptor, PseudoOp.PseudoOpNames pseudoOpNames, out string PseudoOpcode) { PseudoOpcode = null; if (!mStringEntries.TryGetValue(offset, out FormattedStringEntry entry)) { + DebugNotFoundCount++; return null; } if (!entry.CheckDeps(formatter, formatDescriptor, pseudoOpNames)) { //Debug.WriteLine(" stale entry at +" + offset.ToString("x6")); + DebugFoundStaleCount++; return null; } + DebugFoundValidCount++; PseudoOpcode = entry.PseudoOpcode; return entry.Lines; } @@ -137,5 +150,17 @@ namespace SourceGen { formatter, formatDescriptor, pseudoOpNames); mStringEntries[offset] = fse; } + + // Some counters for evaluating efficacy. + public int DebugFoundValidCount { get; private set; } + public int DebugFoundStaleCount { get; private set; } + public int DebugNotFoundCount { get; private set; } + public void DebugResetCounters() { + DebugFoundValidCount = DebugFoundStaleCount = DebugNotFoundCount = 0; + } + public void DebugLogCounters() { + Debug.WriteLine("Operand cache: valid=" + DebugFoundValidCount + ", stale=" + + DebugFoundStaleCount + ", missing=" + DebugNotFoundCount); + } } } diff --git a/SourceGen/LineListGen.cs b/SourceGen/LineListGen.cs index 170488a..39b1e77 100644 --- a/SourceGen/LineListGen.cs +++ b/SourceGen/LineListGen.cs @@ -71,10 +71,15 @@ namespace SourceGen { private PseudoOp.PseudoOpNames mPseudoOpNames; /// - /// Cache of previously-formatted data. The data is stored with references to + /// Cache of previously-formatted operand data. The data is stored with references to /// dependencies, so it should not be necessary to explicitly clear this. /// - private FormattedOperandCache mFormattedLineCache; + private FormattedOperandCache mFormattedLineCache = new FormattedOperandCache(); + + /// + /// Cache of previous-formatted multi-line comment strings. + /// + private FormattedMlcCache mFormattedMlcCache = new FormattedMlcCache(); /// /// Local variable table data extractor. @@ -472,7 +477,6 @@ namespace SourceGen { mPseudoOpNames = opNames; mLineList = new List(); - mFormattedLineCache = new FormattedOperandCache(); mShowCycleCounts = AppSettings.Global.GetBool(AppSettings.FMT_SHOW_CYCLE_COUNTS, false); mLvLookup = new LocalVariableLookup(mProject.LvTables, mProject, null, false, false); @@ -963,7 +967,7 @@ namespace SourceGen { private void GenerateLineList(int startOffset, int endOffset, List lines) { //Debug.WriteLine("GenerateRange [+" + startOffset.ToString("x6") + ",+" + // endOffset.ToString("x6") + "]"); - + DebugResetCacheCounters(); Debug.Assert(startOffset >= 0); Debug.Assert(endOffset >= startOffset); @@ -1053,7 +1057,14 @@ namespace SourceGen { spaceAdded = true; } if (mProject.LongComments.TryGetValue(offset, out MultiLineComment longComment)) { - List formatted = longComment.FormatText(mFormatter, string.Empty); + List formatted = mFormattedMlcCache.GetStringEntry(offset, longComment, + mFormatter); + if (formatted == null) { + Debug.WriteLine("Render " + longComment); + formatted = longComment.FormatText(mFormatter, string.Empty); + mFormattedMlcCache.SetStringEntry(offset, formatted, longComment, + mFormatter); + } StringListToLines(formatted, offset, Line.Type.LongComment, longComment.BackgroundColor, NoteColorMultiplier, lines); spaceAdded = true; @@ -1271,7 +1282,7 @@ namespace SourceGen { } else { Debug.Assert(attr.DataDescriptor != null); if (attr.DataDescriptor.IsString) { - // See if we've already got this one. + // String operand. See if we've already formatted this one. List strLines = mFormattedLineCache.GetStringEntry(offset, mFormatter, attr.DataDescriptor, mPseudoOpNames, out string popcode); if (strLines == null) { @@ -1358,6 +1369,8 @@ namespace SourceGen { } } } + + DebugLogCacheCounters(); } /// @@ -1752,5 +1765,14 @@ namespace SourceGen { return partsArray; } + + public void DebugResetCacheCounters() { + mFormattedLineCache.DebugResetCounters(); + mFormattedMlcCache.DebugResetCounters(); + } + public void DebugLogCacheCounters() { + mFormattedLineCache.DebugLogCounters(); + mFormattedMlcCache.DebugLogCounters(); + } } } diff --git a/SourceGen/SourceGen.csproj b/SourceGen/SourceGen.csproj index 0080c3a..4a2335c 100644 --- a/SourceGen/SourceGen.csproj +++ b/SourceGen/SourceGen.csproj @@ -81,6 +81,7 @@ +