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