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 @@ +