Skip to content

Commit

Permalink
Add formatted MLC cache
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
fadden committed Jul 2, 2024
1 parent b1ca87e commit 54d559a
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 27 deletions.
117 changes: 117 additions & 0 deletions SourceGen/FormattedMlcCache.cs
Original file line number Diff line number Diff line change
@@ -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 {
/// <summary>
/// Holds a cache of formatted multi-line comments.
/// </summary>
/// <remarks>
/// <para>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.</para>
/// </remarks>
public class FormattedMlcCache {
/// <summary>
/// One entry in the cache.
/// </summary>
private class FormattedStringEntry {
public List<string> Lines { get; private set; }

private MultiLineComment mMlc;
private Formatter mFormatter;

public FormattedStringEntry(List<string> lines, MultiLineComment mlc,
Formatter formatter) {
// Can't be sure the list won't change, so duplicate it.
Lines = new List<string>(lines.Count);
foreach (string str in lines) {
Lines.Add(str);
}

mMlc = mlc;
mFormatter = formatter;
}

/// <summary>
/// Checks the entry's dependencies.
/// </summary>
/// <returns>True if the dependencies match.</returns>
public bool CheckDeps(MultiLineComment mlc, Formatter formatter) {
bool ok = (ReferenceEquals(mMlc, mlc) && ReferenceEquals(mFormatter, formatter));
return ok;
}
}

/// <summary>
/// Cached entries, keyed by file offset.
/// </summary>
private Dictionary<int, FormattedStringEntry> mStringEntries =
new Dictionary<int, FormattedStringEntry>();


/// <summary>
/// Retrieves the formatted string data for the specified offset.
/// </summary>
/// <param name="offset">File offset.</param>
/// <param name="formatter">Formatter dependency.</param>
/// <returns>A reference to the string list, or null if the entry is absent or invalid.
/// The caller must not modify the list.</returns>
public List<string> 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;
}

/// <summary>
/// Sets the string data entry for the specified offset.
/// </summary>
/// <param name="offset">File offset.</param>
/// <param name="lines">String data.</param>
/// <param name="mlc">Multi-line comment to be formatted.</param>
/// <param name="formatter">Formatter dependency.</param>
public void SetStringEntry(int offset, List<string> 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);
}
}
}
67 changes: 46 additions & 21 deletions SourceGen/FormattedOperandCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,31 +21,38 @@

namespace SourceGen {
/// <summary>
/// Holds a cache of formatted lines.
/// Holds a cache of formatted operands that may span multiple lines.
/// </summary>
/// <remarks>
/// 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.
/// <para>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.</para>
///
/// 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)
/// <para>The trick is knowing when the cached data must be invalidated. For example, a
/// fully formatted string line must be invalidated if:</para>
/// <list type="bullet">
/// <item>The Formatter changes (different delimiter definition)</item>
/// <item>The FormatDescriptor changes (different length, different text encoding, different
/// type of string)</item>
/// <item>The PseudoOpNames table changes (potentially altering the pseudo-op
/// string used)</item>
/// </list>
///
/// Doing a full .equals() on the various items would reduce performance, so we use a
/// <para>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.</para>
/// <para>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.</para>
/// </remarks>
public class FormattedOperandCache {
private const bool VERBOSE = false;

/// <summary>
/// One entry in the cache.
/// </summary>
private class FormattedStringEntry {
public List<string> Lines { get; private set; }
public string PseudoOpcode { get; private set; }
Expand Down Expand Up @@ -91,6 +98,9 @@ public bool CheckDeps(Formatter formatter, FormatDescriptor formatDescriptor,
}
}

/// <summary>
/// Cached entries, keyed by file offset.
/// </summary>
private Dictionary<int, FormattedStringEntry> mStringEntries =
new Dictionary<int, FormattedStringEntry>();

Expand All @@ -102,20 +112,23 @@ public bool CheckDeps(Formatter formatter, FormatDescriptor formatDescriptor,
/// <param name="formatter">Formatter dependency.</param>
/// <param name="formatDescriptor">FormatDescriptor dependency.</param>
/// <param name="pseudoOpNames">PseudoOpNames dependency.</param>
/// <param name="PseudoOpcode">Pseudo-op for this string.</param>
/// <returns>A reference to the string list. The caller must not modify the
/// list.</returns>
/// <param name="PseudoOpcode">Result: pseudo-op for this string.</param>
/// <returns>A reference to the string list, or null if the entry is absent or invalid.
/// The caller must not modify the list.</returns>
public List<string> 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;
}
Expand All @@ -137,5 +150,17 @@ public void SetStringEntry(int offset, List<string> lines, string pseudoOpcode,
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);
}
}
}
34 changes: 28 additions & 6 deletions SourceGen/LineListGen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,15 @@ public class LineListGen {
private PseudoOp.PseudoOpNames mPseudoOpNames;

/// <summary>
/// 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.
/// </summary>
private FormattedOperandCache mFormattedLineCache;
private FormattedOperandCache mFormattedLineCache = new FormattedOperandCache();

/// <summary>
/// Cache of previous-formatted multi-line comment strings.
/// </summary>
private FormattedMlcCache mFormattedMlcCache = new FormattedMlcCache();

/// <summary>
/// Local variable table data extractor.
Expand Down Expand Up @@ -472,7 +477,6 @@ public LineListGen(DisasmProject proj, DisplayList displayList, Formatter format
mPseudoOpNames = opNames;

mLineList = new List<Line>();
mFormattedLineCache = new FormattedOperandCache();
mShowCycleCounts = AppSettings.Global.GetBool(AppSettings.FMT_SHOW_CYCLE_COUNTS,
false);
mLvLookup = new LocalVariableLookup(mProject.LvTables, mProject, null, false, false);
Expand Down Expand Up @@ -963,7 +967,7 @@ private static List<Line> GenerateHeaderLines(DisasmProject proj, Formatter form
private void GenerateLineList(int startOffset, int endOffset, List<Line> lines) {
//Debug.WriteLine("GenerateRange [+" + startOffset.ToString("x6") + ",+" +
// endOffset.ToString("x6") + "]");

DebugResetCacheCounters();

Debug.Assert(startOffset >= 0);
Debug.Assert(endOffset >= startOffset);
Expand Down Expand Up @@ -1053,7 +1057,14 @@ private void GenerateLineList(int startOffset, int endOffset, List<Line> lines)
spaceAdded = true;
}
if (mProject.LongComments.TryGetValue(offset, out MultiLineComment longComment)) {
List<string> formatted = longComment.FormatText(mFormatter, string.Empty);
List<string> 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;
Expand Down Expand Up @@ -1271,7 +1282,7 @@ private void GenerateLineList(int startOffset, int endOffset, List<Line> lines)
} 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<string> strLines = mFormattedLineCache.GetStringEntry(offset,
mFormatter, attr.DataDescriptor, mPseudoOpNames, out string popcode);
if (strLines == null) {
Expand Down Expand Up @@ -1358,6 +1369,8 @@ private void GenerateLineList(int startOffset, int endOffset, List<Line> lines)
}
}
}

DebugLogCacheCounters();
}

/// <summary>
Expand Down Expand Up @@ -1752,5 +1765,14 @@ private FormattedParts[] GenerateStringLines(int offset, string popcode,

return partsArray;
}

public void DebugResetCacheCounters() {
mFormattedLineCache.DebugResetCounters();
mFormattedMlcCache.DebugResetCounters();
}
public void DebugLogCacheCounters() {
mFormattedLineCache.DebugLogCounters();
mFormattedMlcCache.DebugLogCounters();
}
}
}
1 change: 1 addition & 0 deletions SourceGen/SourceGen.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
<Compile Include="AsmGen\LabelLocalizer.cs" />
<Compile Include="DailyTips.cs" />
<Compile Include="Exporter.cs" />
<Compile Include="FormattedMlcCache.cs" />
<Compile Include="FormattedOperandCache.cs" />
<Compile Include="LabelFileGenerator.cs" />
<Compile Include="LocalVariableLookup.cs" />
Expand Down

0 comments on commit 54d559a

Please sign in to comment.