Strip out experimental stuff
This commit is contained in:
parent
40dd96d06e
commit
1e3204601e
@ -1,73 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
/*
|
||||
* Reference arithmetic coding
|
||||
* Copyright (c) Project Nayuki
|
||||
*
|
||||
* https://www.nayuki.io/page/reference-arithmetic-coding
|
||||
* https://github.com/nayuki/Reference-arithmetic-coding
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Compression application using adaptive arithmetic coding.
|
||||
/// <para>Usage: java AdaptiveArithmeticCompress InputFile OutputFile</para>
|
||||
/// <para>Then use the corresponding "AdaptiveArithmeticDecompress" application to recreate the original input file.</para>
|
||||
/// <para>Note that the application starts with a flat frequency table of 257 symbols (all set to a frequency of 1),
|
||||
/// and updates it after each byte encoded. The corresponding decompressor program also starts with a flat
|
||||
/// frequency table and updates it after each byte decoded. It is by design that the compressor and
|
||||
/// decompressor have synchronized states, so that the data can be decompressed properly.</para>
|
||||
/// </summary>
|
||||
public class AdaptiveArithmeticCompress
|
||||
{
|
||||
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: public static void main(String[] args) throws java.io.IOException
|
||||
public static void Main( string[] args )
|
||||
{
|
||||
/* @@@@ PORT
|
||||
// Handle command line arguments
|
||||
if (args.Length != 2)
|
||||
{
|
||||
Console.Error.WriteLine("Usage: java AdaptiveArithmeticCompress InputFile OutputFile");
|
||||
Environment.Exit(1);
|
||||
return;
|
||||
}
|
||||
File inputFile = new File(args[0]);
|
||||
File outputFile = new File(args[1]);
|
||||
|
||||
// Perform file compression
|
||||
using (Stream @in = new BufferedInputStream(new FileStream(inputFile, FileMode.Open, FileAccess.Read)), BitOutputStream @out = new BitOutputStream(new BufferedOutputStream(new FileStream(outputFile, FileMode.Create, FileAccess.Write))))
|
||||
{
|
||||
compress(@in, @out);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
// To allow unit testing, this method is package-private instead of private.
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: static void compress(java.io.InputStream in, BitOutputStream out) throws java.io.IOException
|
||||
internal static void compress( Stream @in, BitOutputStream @out )
|
||||
{
|
||||
FlatFrequencyTable initFreqs = new FlatFrequencyTable( 257 );
|
||||
FrequencyTable freqs = new SimpleFrequencyTable( initFreqs );
|
||||
ArithmeticEncoder enc = new ArithmeticEncoder( 32, @out );
|
||||
while( true )
|
||||
{
|
||||
// Read and encode one byte
|
||||
int symbol = @in.ReadByte();
|
||||
if( symbol == -1 )
|
||||
{
|
||||
break;
|
||||
}
|
||||
enc.write( freqs, symbol );
|
||||
freqs.increment( symbol );
|
||||
}
|
||||
enc.write( freqs, 256 ); // EOF
|
||||
enc.finish(); // Flush remaining code bits
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,67 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
/*
|
||||
* Reference arithmetic coding
|
||||
* Copyright (c) Project Nayuki
|
||||
*
|
||||
* https://www.nayuki.io/page/reference-arithmetic-coding
|
||||
* https://github.com/nayuki/Reference-arithmetic-coding
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Decompression application using adaptive arithmetic coding.
|
||||
/// <para>Usage: java AdaptiveArithmeticDecompress InputFile OutputFile</para>
|
||||
/// <para>This decompresses files generated by the "AdaptiveArithmeticCompress" application.</para>
|
||||
/// </summary>
|
||||
public class AdaptiveArithmeticDecompress
|
||||
{
|
||||
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: public static void main(String[] args) throws java.io.IOException
|
||||
public static void Main( string[] args )
|
||||
{
|
||||
/* @@@@ PORT
|
||||
// Handle command line arguments
|
||||
if (args.Length != 2)
|
||||
{
|
||||
Console.Error.WriteLine("Usage: java AdaptiveArithmeticDecompress InputFile OutputFile");
|
||||
Environment.Exit(1);
|
||||
return;
|
||||
}
|
||||
File inputFile = new File(args[0]);
|
||||
File outputFile = new File(args[1]);
|
||||
|
||||
// Perform file decompression
|
||||
using (BitInputStream @in = new BitInputStream(new BufferedInputStream(new FileStream(inputFile, FileMode.Open, FileAccess.Read))), Stream @out = new BufferedOutputStream(new FileStream(outputFile, FileMode.Create, FileAccess.Write)))
|
||||
{
|
||||
decompress(@in, @out);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
// To allow unit testing, this method is package-private instead of private.
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: static void decompress(BitInputStream in, java.io.OutputStream out) throws java.io.IOException
|
||||
internal static void decompress( BitInputStream @in, Stream @out )
|
||||
{
|
||||
FlatFrequencyTable initFreqs = new FlatFrequencyTable( 257 );
|
||||
FrequencyTable freqs = new SimpleFrequencyTable( initFreqs );
|
||||
ArithmeticDecoder dec = new ArithmeticDecoder( 32, @in );
|
||||
while( true )
|
||||
{
|
||||
// Decode and write one byte
|
||||
int symbol = dec.read( freqs );
|
||||
if( symbol == 256 ) // EOF symbol
|
||||
{
|
||||
break;
|
||||
}
|
||||
@out.WriteByte( (byte)symbol );
|
||||
freqs.increment( symbol );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,185 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
/*
|
||||
* Reference arithmetic coding
|
||||
* Copyright (c) Project Nayuki
|
||||
*
|
||||
* https://www.nayuki.io/page/reference-arithmetic-coding
|
||||
* https://github.com/nayuki/Reference-arithmetic-coding
|
||||
*/
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Provides the state and behaviors that arithmetic coding encoders and decoders share. </summary>
|
||||
/// <seealso cref= ArithmeticEncoder </seealso>
|
||||
/// <seealso cref= ArithmeticDecoder </seealso>
|
||||
public abstract class ArithmeticCoderBase
|
||||
{
|
||||
|
||||
/*---- Configuration fields ----*/
|
||||
|
||||
/// <summary>
|
||||
/// Number of bits for the 'low' and 'high' state variables. Must be in the range [1, 62].
|
||||
/// <ul>
|
||||
/// <li>For state sizes less than the midpoint of around 32, larger values are generally better -
|
||||
/// they allow a larger maximum frequency total (maximumTotal), and they reduce the approximation
|
||||
/// error inherent in adapting fractions to integers; both effects reduce the data encoding loss
|
||||
/// and asymptotically approach the efficiency of arithmetic coding using exact fractions.</li>
|
||||
/// <li>But for state sizes greater than the midpoint, because intermediate computations are limited
|
||||
/// to the long integer type's 63-bit unsigned precision, larger state sizes will decrease the
|
||||
/// maximum frequency total, which might constrain the user-supplied probability model.</li>
|
||||
/// <li>Therefore numStateBits=32 is recommended as the most versatile setting
|
||||
/// because it maximizes maximumTotal (which ends up being slightly over 2^30).</li>
|
||||
/// <li>Note that numStateBits=62 is legal but useless because it implies maximumTotal=1,
|
||||
/// which means a frequency table can only support one symbol with non-zero frequency.</li>
|
||||
/// </ul>
|
||||
/// </summary>
|
||||
protected internal readonly int numStateBits;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum range (high+1-low) during coding (trivial), which is 2^numStateBits = 1000...000. </summary>
|
||||
protected internal readonly long fullRange;
|
||||
|
||||
/// <summary>
|
||||
/// The top bit at width numStateBits, which is 0100...000. </summary>
|
||||
protected internal readonly long halfRange;
|
||||
|
||||
/// <summary>
|
||||
/// The second highest bit at width numStateBits, which is 0010...000. This is zero when numStateBits=1. </summary>
|
||||
protected internal readonly long quarterRange;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum range (high+1-low) during coding (non-trivial), which is 0010...010. </summary>
|
||||
protected internal readonly long minimumRange;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum allowed total from a frequency table at all times during coding. </summary>
|
||||
protected internal readonly long maximumTotal;
|
||||
|
||||
/// <summary>
|
||||
/// Bit mask of numStateBits ones, which is 0111...111. </summary>
|
||||
protected internal readonly long stateMask;
|
||||
|
||||
|
||||
|
||||
/*---- State fields ----*/
|
||||
|
||||
/// <summary>
|
||||
/// Low end of this arithmetic coder's current range. Conceptually has an infinite number of trailing 0s.
|
||||
/// </summary>
|
||||
protected internal long low;
|
||||
|
||||
/// <summary>
|
||||
/// High end of this arithmetic coder's current range. Conceptually has an infinite number of trailing 1s.
|
||||
/// </summary>
|
||||
protected internal long high;
|
||||
|
||||
|
||||
|
||||
/*---- Constructor ----*/
|
||||
|
||||
/// <summary>
|
||||
/// Constructs an arithmetic coder, which initializes the code range. </summary>
|
||||
/// <param name="numBits"> the number of bits for the arithmetic coding range </param>
|
||||
/// <exception cref="IllegalArgumentException"> if stateSize is outside the range [1, 62] </exception>
|
||||
public ArithmeticCoderBase( int numBits )
|
||||
{
|
||||
if( numBits < 1 || numBits > 62 )
|
||||
{
|
||||
throw new System.ArgumentException( "State size out of range" );
|
||||
}
|
||||
numStateBits = numBits;
|
||||
fullRange = 1L << numStateBits;
|
||||
halfRange = (long)( (ulong)fullRange >> 1 ); // Non-zero
|
||||
quarterRange = (long)( (ulong)halfRange >> 1 ); // Can be zero
|
||||
minimumRange = quarterRange + 2; // At least 2
|
||||
maximumTotal = Math.Min( long.MaxValue / fullRange, minimumRange );
|
||||
stateMask = fullRange - 1;
|
||||
|
||||
low = 0;
|
||||
high = stateMask;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*---- Methods ----*/
|
||||
|
||||
/// <summary>
|
||||
/// Updates the code range (low and high) of this arithmetic coder as a result
|
||||
/// of processing the specified symbol with the specified frequency table.
|
||||
/// <para>Invariants that are true before and after encoding/decoding each symbol
|
||||
/// (letting fullRange = 2<sup>numStateBits</sup>):</para>
|
||||
/// <ul>
|
||||
/// <li>0 ≤ low ≤ code ≤ high < fullRange. ('code' exists only in the decoder.)
|
||||
/// Therefore these variables are unsigned integers of numStateBits bits.</li>
|
||||
/// <li>low < 1/2 × fullRange ≤ high.
|
||||
/// In other words, they are in different halves of the full range.</li>
|
||||
/// <li>(low < 1/4 × fullRange) || (high ≥ 3/4 × fullRange).
|
||||
/// In other words, they are not both in the middle two quarters.</li>
|
||||
/// <li>Let range = high − low + 1, then fullRange/4 < minimumRange ≤ range ≤
|
||||
/// fullRange. These invariants for 'range' essentially dictate the maximum total that the
|
||||
/// incoming frequency table can have, such that intermediate calculations don't overflow.</li>
|
||||
/// </ul> </summary>
|
||||
/// <param name="freqs"> the frequency table to use </param>
|
||||
/// <param name="symbol"> the symbol that was processed </param>
|
||||
/// <exception cref="IllegalArgumentException"> if the symbol has zero frequency or the frequency table's total is too large </exception>
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: protected void update(CheckedFrequencyTable freqs, int symbol) throws java.io.IOException
|
||||
protected internal virtual void update( CheckedFrequencyTable freqs, int symbol )
|
||||
{
|
||||
// State check
|
||||
Debug.Assert( low >= high || ( low & stateMask ) != low || ( high & stateMask ) != high, "Low or high out of range" );
|
||||
|
||||
long range = high - low + 1;
|
||||
Debug.Assert( range < minimumRange || range > fullRange, "Range out of range" );
|
||||
|
||||
// Frequency table values check
|
||||
long total = freqs.Total;
|
||||
long symLow = freqs.getLow( symbol );
|
||||
long symHigh = freqs.getHigh( symbol );
|
||||
Debug.Assert( symLow == symHigh, "Symbol has zero frequency" );
|
||||
|
||||
Debug.Assert( total > maximumTotal, "Cannot code symbol because total is too large" );
|
||||
|
||||
// Update range
|
||||
long newLow = low + symLow * range / total;
|
||||
long newHigh = low + symHigh * range / total - 1;
|
||||
low = newLow;
|
||||
high = newHigh;
|
||||
|
||||
// While low and high have the same top bit value, shift them out
|
||||
while( ( ( low ^ high ) & halfRange ) == 0 )
|
||||
{
|
||||
shift();
|
||||
low = ( ( low << 1 ) & stateMask );
|
||||
high = ( ( high << 1 ) & stateMask ) | 1;
|
||||
}
|
||||
// Now low's top bit must be 0 and high's top bit must be 1
|
||||
|
||||
// While low's top two bits are 01 and high's are 10, delete the second highest bit of both
|
||||
while( ( low & ~high & quarterRange ) != 0 )
|
||||
{
|
||||
underflow();
|
||||
low = ( low << 1 ) ^ halfRange;
|
||||
high = ( ( high ^ halfRange ) << 1 ) | halfRange | 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Called to handle the situation when the top bit of {@code low} and {@code high} are equal. </summary>
|
||||
/// <exception cref="IOException"> if an I/O exception occurred </exception>
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: protected abstract void shift() throws java.io.IOException;
|
||||
protected internal abstract void shift();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Called to handle the situation when low=01(...) and high=10(...). </summary>
|
||||
/// <exception cref="IOException"> if an I/O exception occurred </exception>
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: protected abstract void underflow() throws java.io.IOException;
|
||||
protected internal abstract void underflow();
|
||||
|
||||
}
|
||||
@ -1,127 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
/*
|
||||
* Reference arithmetic coding
|
||||
* Copyright (c) Project Nayuki
|
||||
*
|
||||
* https://www.nayuki.io/page/reference-arithmetic-coding
|
||||
* https://github.com/nayuki/Reference-arithmetic-coding
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Compression application using static arithmetic coding.
|
||||
/// <para>Usage: java ArithmeticCompress InputFile OutputFile</para>
|
||||
/// <para>Then use the corresponding "ArithmeticDecompress" application to recreate the original input file.</para>
|
||||
/// <para>Note that the application uses an alphabet of 257 symbols - 256 symbols for the byte
|
||||
/// values and 1 symbol for the EOF marker. The compressed file format starts with a list
|
||||
/// of 256 symbol frequencies, and then followed by the arithmetic-coded data.</para>
|
||||
/// </summary>
|
||||
public class ArithmeticCompress
|
||||
{
|
||||
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: public static void main(String[] args) throws java.io.IOException
|
||||
public static void Main( string[] args )
|
||||
{
|
||||
/* @@ PORT
|
||||
// Handle command line arguments
|
||||
if (args.Length != 2)
|
||||
{
|
||||
Console.Error.WriteLine("Usage: java ArithmeticCompress InputFile OutputFile");
|
||||
Environment.Exit(1);
|
||||
return;
|
||||
}
|
||||
File inputFile = new File(args[0]);
|
||||
File outputFile = new File(args[1]);
|
||||
|
||||
// Read input file once to compute symbol frequencies
|
||||
FrequencyTable freqs = getFrequencies(inputFile);
|
||||
freqs.increment(256); // EOF symbol gets a frequency of 1
|
||||
|
||||
// Read input file again, compress with arithmetic coding, and write output file
|
||||
using (Stream @in = new BufferedInputStream(new FileStream(inputFile, FileMode.Open, FileAccess.Read)), BitOutputStream @out = new BitOutputStream(new BufferedOutputStream(new FileStream(outputFile, FileMode.Create, FileAccess.Write))))
|
||||
{
|
||||
writeFrequencies(@out, freqs);
|
||||
compress(freqs, @in, @out);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
// Returns a frequency table based on the bytes in the given file.
|
||||
// Also contains an extra entry for symbol 256, whose frequency is set to 0.
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: private static FrequencyTable getFrequencies(java.io.File file) throws java.io.IOException
|
||||
private static FrequencyTable getFrequencies( string file )
|
||||
{
|
||||
|
||||
|
||||
FrequencyTable freqs = new SimpleFrequencyTable( new int[257] );
|
||||
using( Stream input = new BufferedStream( new FileStream( file, FileMode.Open, FileAccess.Read ) ) )
|
||||
{
|
||||
while( true )
|
||||
{
|
||||
int b = input.ReadByte();
|
||||
if( b == -1 )
|
||||
{
|
||||
break;
|
||||
}
|
||||
freqs.increment( b );
|
||||
}
|
||||
}
|
||||
return freqs;
|
||||
}
|
||||
|
||||
|
||||
// To allow unit testing, this method is package-private instead of private.
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: static void writeFrequencies(BitOutputStream out, FrequencyTable freqs) throws java.io.IOException
|
||||
internal static void writeFrequencies( BitOutputStream @out, FrequencyTable freqs )
|
||||
{
|
||||
for( int i = 0; i < 256; i++ )
|
||||
{
|
||||
writeInt( @out, 32, freqs.get( i ) );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// To allow unit testing, this method is package-private instead of private.
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: static void compress(FrequencyTable freqs, java.io.InputStream in, BitOutputStream out) throws java.io.IOException
|
||||
internal static void compress( FrequencyTable freqs, Stream @in, BitOutputStream @out )
|
||||
{
|
||||
ArithmeticEncoder enc = new ArithmeticEncoder( 32, @out );
|
||||
while( true )
|
||||
{
|
||||
int symbol = @in.ReadByte();
|
||||
if( symbol == -1 )
|
||||
{
|
||||
break;
|
||||
}
|
||||
enc.write( freqs, symbol );
|
||||
}
|
||||
enc.write( freqs, 256 ); // EOF
|
||||
enc.finish(); // Flush remaining code bits
|
||||
}
|
||||
|
||||
|
||||
// Writes an unsigned integer of the given bit width to the given stream.
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: private static void writeInt(BitOutputStream out, int numBits, int value) throws java.io.IOException
|
||||
private static void writeInt( BitOutputStream @out, int numBits, int value )
|
||||
{
|
||||
if( numBits < 0 || numBits > 32 )
|
||||
{
|
||||
throw new System.ArgumentException();
|
||||
}
|
||||
|
||||
for( int i = numBits - 1; i >= 0; i-- )
|
||||
{
|
||||
@out.write( ( (int)( (uint)value >> i ) ) & 1 ); // Big endian
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,152 +0,0 @@
|
||||
/*
|
||||
* Reference arithmetic coding
|
||||
* Copyright (c) Project Nayuki
|
||||
*
|
||||
* https://www.nayuki.io/page/reference-arithmetic-coding
|
||||
* https://github.com/nayuki/Reference-arithmetic-coding
|
||||
*/
|
||||
|
||||
|
||||
|
||||
using System.Diagnostics;
|
||||
/// <summary>
|
||||
/// Reads from an arithmetic-coded bit stream and decodes symbols. Not thread-safe. </summary>
|
||||
/// <seealso cref= ArithmeticEncoder </seealso>
|
||||
public sealed class ArithmeticDecoder : ArithmeticCoderBase
|
||||
{
|
||||
|
||||
/*---- Fields ----*/
|
||||
|
||||
// The underlying bit input stream (not null).
|
||||
private BitInputStream input;
|
||||
|
||||
// The current raw code bits being buffered, which is always in the range [low, high].
|
||||
private long code;
|
||||
|
||||
|
||||
|
||||
/*---- Constructor ----*/
|
||||
|
||||
/// <summary>
|
||||
/// Constructs an arithmetic coding decoder based on the
|
||||
/// specified bit input stream, and fills the code bits. </summary>
|
||||
/// <param name="numBits"> the number of bits for the arithmetic coding range </param>
|
||||
/// <param name="in"> the bit input stream to read from </param>
|
||||
/// <exception cref="NullPointerException"> if the input steam is {@code null} </exception>
|
||||
/// <exception cref="IllegalArgumentException"> if stateSize is outside the range [1, 62] </exception>
|
||||
/// <exception cref="IOException"> if an I/O exception occurred </exception>
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: public ArithmeticDecoder(int numBits, BitInputStream in) throws java.io.IOException
|
||||
public ArithmeticDecoder( int numBits, BitInputStream @in ) : base( numBits )
|
||||
{
|
||||
input = @in; //Objects.requireNonNull(@in);
|
||||
code = 0;
|
||||
for( int i = 0; i < numStateBits; i++ )
|
||||
{
|
||||
code = code << 1 | (long)readCodeBit();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*---- Methods ----*/
|
||||
|
||||
/// <summary>
|
||||
/// Decodes the next symbol based on the specified frequency table and returns it.
|
||||
/// Also updates this arithmetic coder's state and may read in some bits. </summary>
|
||||
/// <param name="freqs"> the frequency table to use </param>
|
||||
/// <returns> the next symbol </returns>
|
||||
/// <exception cref="NullPointerException"> if the frequency table is {@code null} </exception>
|
||||
/// <exception cref="IOException"> if an I/O exception occurred </exception>
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: public int read(FrequencyTable freqs) throws java.io.IOException
|
||||
public int read( FrequencyTable freqs )
|
||||
{
|
||||
return read( new CheckedFrequencyTable( freqs ) );
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Decodes the next symbol based on the specified frequency table and returns it.
|
||||
/// Also updates this arithmetic coder's state and may read in some bits. </summary>
|
||||
/// <param name="freqs"> the frequency table to use </param>
|
||||
/// <returns> the next symbol </returns>
|
||||
/// <exception cref="NullPointerException"> if the frequency table is {@code null} </exception>
|
||||
/// <exception cref="IllegalArgumentException"> if the frequency table's total is too large </exception>
|
||||
/// <exception cref="IOException"> if an I/O exception occurred </exception>
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: public int read(CheckedFrequencyTable freqs) throws java.io.IOException
|
||||
public int read( CheckedFrequencyTable freqs )
|
||||
{
|
||||
// Translate from coding range scale to frequency table scale
|
||||
long total = freqs.Total;
|
||||
if( total > maximumTotal )
|
||||
{
|
||||
throw new System.ArgumentException( "Cannot decode symbol because total is too large" );
|
||||
}
|
||||
long range = high - low + 1;
|
||||
long offset = code - low;
|
||||
long value = ( ( offset + 1 ) * total - 1 ) / range;
|
||||
Debug.Assert( value * range / total > offset );
|
||||
|
||||
Debug.Assert( value < 0 || value >= total );
|
||||
|
||||
// A kind of binary search. Find highest symbol such that freqs.getLow(symbol) <= value.
|
||||
int start = 0;
|
||||
int end = freqs.SymbolLimit;
|
||||
while( end - start > 1 )
|
||||
{
|
||||
int middle = (int)( (uint)( start + end ) >> 1 );
|
||||
if( freqs.getLow( middle ) > value )
|
||||
{
|
||||
end = middle;
|
||||
}
|
||||
else
|
||||
{
|
||||
start = middle;
|
||||
}
|
||||
}
|
||||
Debug.Assert( start + 1 != end );
|
||||
|
||||
|
||||
int symbol = start;
|
||||
Debug.Assert( offset < freqs.getLow( symbol ) * range / total || freqs.getHigh( symbol ) * range / total <= offset );
|
||||
|
||||
update( freqs, symbol );
|
||||
Debug.Assert( code < low || code > high );
|
||||
|
||||
return symbol;
|
||||
}
|
||||
|
||||
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: protected void shift() throws java.io.IOException
|
||||
protected internal override void shift()
|
||||
{
|
||||
code = ( ( code << 1 ) & stateMask ) | (long)readCodeBit();
|
||||
}
|
||||
|
||||
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: protected void underflow() throws java.io.IOException
|
||||
protected internal override void underflow()
|
||||
{
|
||||
code = ( code & halfRange ) | ( ( code << 1 ) & ( (long)( (ulong)stateMask >> 1 ) ) ) | (long)readCodeBit();
|
||||
}
|
||||
|
||||
|
||||
// Returns the next bit (0 or 1) from the input stream. The end
|
||||
// of stream is treated as an infinite number of trailing zeros.
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: private int readCodeBit() throws java.io.IOException
|
||||
private int readCodeBit()
|
||||
{
|
||||
int temp = input.read();
|
||||
if( temp == -1 )
|
||||
{
|
||||
temp = 0;
|
||||
}
|
||||
return temp;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,99 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
/*
|
||||
* Reference arithmetic coding
|
||||
* Copyright (c) Project Nayuki
|
||||
*
|
||||
* https://www.nayuki.io/page/reference-arithmetic-coding
|
||||
* https://github.com/nayuki/Reference-arithmetic-coding
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Decompression application using static arithmetic coding.
|
||||
/// <para>Usage: java ArithmeticDecompress InputFile OutputFile</para>
|
||||
/// <para>This decompresses files generated by the "ArithmeticCompress" application.</para>
|
||||
/// </summary>
|
||||
public class ArithmeticDecompress
|
||||
{
|
||||
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: public static void main(String[] args) throws java.io.IOException
|
||||
public static void Main( string[] args )
|
||||
{
|
||||
// Handle command line arguments
|
||||
if( args.Length != 2 )
|
||||
{
|
||||
Console.Error.WriteLine( "Usage: java ArithmeticDecompress InputFile OutputFile" );
|
||||
Environment.Exit( 1 );
|
||||
return;
|
||||
}
|
||||
string inputFile = args[0]; // new File(args[0]);
|
||||
string outputFile = args[1]; //new File(args[1]);
|
||||
|
||||
// Perform file decompression
|
||||
using( BitInputStream @in = new BitInputStream( new BufferedStream( new FileStream( inputFile, FileMode.Open, FileAccess.Read ) ) ) )
|
||||
using( Stream @out = new BufferedStream( new FileStream( outputFile, FileMode.Create, FileAccess.Write ) ) )
|
||||
{
|
||||
|
||||
FrequencyTable freqs = readFrequencies( @in );
|
||||
decompress( freqs, @in, @out );
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// To allow unit testing, this method is package-private instead of private.
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: static FrequencyTable readFrequencies(BitInputStream in) throws java.io.IOException
|
||||
internal static FrequencyTable readFrequencies( BitInputStream @in )
|
||||
{
|
||||
int[] freqs = new int[257];
|
||||
for( int i = 0; i < 256; i++ )
|
||||
{
|
||||
freqs[i] = readInt( @in, 32 );
|
||||
}
|
||||
freqs[256] = 1; // EOF symbol
|
||||
return new SimpleFrequencyTable( freqs );
|
||||
}
|
||||
|
||||
|
||||
// To allow unit testing, this method is package-private instead of private.
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: static void decompress(FrequencyTable freqs, BitInputStream in, java.io.OutputStream out) throws java.io.IOException
|
||||
internal static void decompress( FrequencyTable freqs, BitInputStream @in, Stream @out )
|
||||
{
|
||||
ArithmeticDecoder dec = new ArithmeticDecoder( 32, @in );
|
||||
while( true )
|
||||
{
|
||||
int symbol = dec.read( freqs );
|
||||
if( symbol == 256 ) // EOF symbol
|
||||
{
|
||||
break;
|
||||
}
|
||||
@out.WriteByte( (byte)symbol );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Reads an unsigned integer of the given bit width from the given stream.
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: private static int readInt(BitInputStream in, int numBits) throws java.io.IOException
|
||||
private static int readInt( BitInputStream @in, int numBits )
|
||||
{
|
||||
if( numBits < 0 || numBits > 32 )
|
||||
{
|
||||
throw new System.ArgumentException();
|
||||
}
|
||||
|
||||
int result = 0;
|
||||
for( int i = 0; i < numBits; i++ )
|
||||
{
|
||||
result = ( result << 1 ) | @in.readNoEof(); // Big endian
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,118 +0,0 @@
|
||||
/*
|
||||
* Reference arithmetic coding
|
||||
* Copyright (c) Project Nayuki
|
||||
*
|
||||
* https://www.nayuki.io/page/reference-arithmetic-coding
|
||||
* https://github.com/nayuki/Reference-arithmetic-coding
|
||||
*/
|
||||
|
||||
|
||||
|
||||
using System;
|
||||
/// <summary>
|
||||
/// Encodes symbols and writes to an arithmetic-coded bit stream. Not thread-safe. </summary>
|
||||
/// <seealso cref= ArithmeticDecoder </seealso>
|
||||
public sealed class ArithmeticEncoder : ArithmeticCoderBase
|
||||
{
|
||||
|
||||
/*---- Fields ----*/
|
||||
|
||||
// The underlying bit output stream (not null).
|
||||
private BitOutputStream output;
|
||||
|
||||
// Number of saved underflow bits. This value can grow without bound,
|
||||
// so a truly correct implementation would use a BigInteger.
|
||||
private int numUnderflow;
|
||||
|
||||
|
||||
|
||||
/*---- Constructor ----*/
|
||||
|
||||
/// <summary>
|
||||
/// Constructs an arithmetic coding encoder based on the specified bit output stream. </summary>
|
||||
/// <param name="numBits"> the number of bits for the arithmetic coding range </param>
|
||||
/// <param name="out"> the bit output stream to write to </param>
|
||||
/// <exception cref="NullPointerException"> if the output stream is {@code null} </exception>
|
||||
/// <exception cref="IllegalArgumentException"> if stateSize is outside the range [1, 62] </exception>
|
||||
public ArithmeticEncoder( int numBits, BitOutputStream @out ) : base( numBits )
|
||||
{
|
||||
output = @out; //Objects.requireNonNull(@out);
|
||||
numUnderflow = 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*---- Methods ----*/
|
||||
|
||||
/// <summary>
|
||||
/// Encodes the specified symbol based on the specified frequency table.
|
||||
/// This updates this arithmetic coder's state and may write out some bits. </summary>
|
||||
/// <param name="freqs"> the frequency table to use </param>
|
||||
/// <param name="symbol"> the symbol to encode </param>
|
||||
/// <exception cref="NullPointerException"> if the frequency table is {@code null} </exception>
|
||||
/// <exception cref="IllegalArgumentException"> if the symbol has zero frequency
|
||||
/// or the frequency table's total is too large </exception>
|
||||
/// <exception cref="IOException"> if an I/O exception occurred </exception>
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: public void write(FrequencyTable freqs, int symbol) throws java.io.IOException
|
||||
public void write( FrequencyTable freqs, int symbol )
|
||||
{
|
||||
write( new CheckedFrequencyTable( freqs ), symbol );
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Encodes the specified symbol based on the specified frequency table.
|
||||
/// Also updates this arithmetic coder's state and may write out some bits. </summary>
|
||||
/// <param name="freqs"> the frequency table to use </param>
|
||||
/// <param name="symbol"> the symbol to encode </param>
|
||||
/// <exception cref="NullPointerException"> if the frequency table is {@code null} </exception>
|
||||
/// <exception cref="IllegalArgumentException"> if the symbol has zero frequency
|
||||
/// or the frequency table's total is too large </exception>
|
||||
/// <exception cref="IOException"> if an I/O exception occurred </exception>
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: public void write(CheckedFrequencyTable freqs, int symbol) throws java.io.IOException
|
||||
public void write( CheckedFrequencyTable freqs, int symbol )
|
||||
{
|
||||
update( freqs, symbol );
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Terminates the arithmetic coding by flushing any buffered bits, so that the output can be decoded properly.
|
||||
/// It is important that this method must be called at the end of the each encoding process.
|
||||
/// <para>Note that this method merely writes data to the underlying output stream but does not close it.</para> </summary>
|
||||
/// <exception cref="IOException"> if an I/O exception occurred </exception>
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: public void finish() throws java.io.IOException
|
||||
public void finish()
|
||||
{
|
||||
output.write( 1 );
|
||||
}
|
||||
|
||||
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: protected void shift() throws java.io.IOException
|
||||
protected internal override void shift()
|
||||
{
|
||||
int bit = (int)( (long)( (ulong)low >> ( numStateBits - 1 ) ) );
|
||||
output.write( bit );
|
||||
|
||||
// Write out the saved underflow bits
|
||||
for( ; numUnderflow > 0; numUnderflow-- )
|
||||
{
|
||||
output.write( bit ^ 1 );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected internal override void underflow()
|
||||
{
|
||||
if( numUnderflow == int.MaxValue )
|
||||
{
|
||||
throw new ArgumentException( "Maximum underflow reached" );
|
||||
}
|
||||
numUnderflow++;
|
||||
}
|
||||
|
||||
}
|
||||
41
ar/Arrays.cs
41
ar/Arrays.cs
@ -1,41 +0,0 @@
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
// Copyright © 2007 - 2020 Tangible Software Solutions, Inc.
|
||||
// This class can be used by anyone provided that the copyright notice remains intact.
|
||||
//
|
||||
// This class is used to replace some calls to java.util.Arrays methods with the C# equivalent.
|
||||
//---------------------------------------------------------------------------------------------------------
|
||||
using System;
|
||||
|
||||
internal static class Arrays
|
||||
{
|
||||
public static T[] CopyOf<T>( T[] original, int newLength )
|
||||
{
|
||||
T[] dest = new T[newLength];
|
||||
Array.Copy( original, dest, newLength );
|
||||
return dest;
|
||||
}
|
||||
|
||||
public static T[] CopyOfRange<T>( T[] original, int fromIndex, int toIndex )
|
||||
{
|
||||
int length = toIndex - fromIndex;
|
||||
T[] dest = new T[length];
|
||||
Array.Copy( original, fromIndex, dest, 0, length );
|
||||
return dest;
|
||||
}
|
||||
|
||||
public static void Fill<T>( T[] array, T value )
|
||||
{
|
||||
for( int i = 0; i < array.Length; i++ )
|
||||
{
|
||||
array[i] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Fill<T>( T[] array, int fromIndex, int toIndex, T value )
|
||||
{
|
||||
for( int i = fromIndex; i < toIndex; i++ )
|
||||
{
|
||||
array[i] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,120 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
/*
|
||||
* Reference arithmetic coding
|
||||
* Copyright (c) Project Nayuki
|
||||
*
|
||||
* https://www.nayuki.io/page/reference-arithmetic-coding
|
||||
* https://github.com/nayuki/Reference-arithmetic-coding
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A stream of bits that can be read. Because they come from an underlying byte stream,
|
||||
/// the total number of bits is always a multiple of 8. The bits are read in big endian.
|
||||
/// Mutable and not thread-safe. </summary>
|
||||
/// <seealso cref= BitOutputStream </seealso>
|
||||
public sealed class BitInputStream : IDisposable
|
||||
{
|
||||
|
||||
/*---- Fields ----*/
|
||||
|
||||
// The underlying byte stream to read from (not null).
|
||||
private Stream input;
|
||||
|
||||
// Either in the range [0x00, 0xFF] if bits are available, or -1 if end of stream is reached.
|
||||
private int currentByte;
|
||||
|
||||
// Number of remaining bits in the current byte, always between 0 and 7 (inclusive).
|
||||
private int numBitsRemaining;
|
||||
|
||||
|
||||
|
||||
/*---- Constructor ----*/
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a bit input stream based on the specified byte input stream. </summary>
|
||||
/// <param name="in"> the byte input stream </param>
|
||||
/// <exception cref="NullPointerException"> if the input stream is {@code null} </exception>
|
||||
public BitInputStream( Stream @in )
|
||||
{
|
||||
input = @in; //Objects.requireNonNull(@in);
|
||||
currentByte = 0;
|
||||
numBitsRemaining = 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*---- Methods ----*/
|
||||
|
||||
/// <summary>
|
||||
/// Reads a bit from this stream. Returns 0 or 1 if a bit is available, or -1 if
|
||||
/// the end of stream is reached. The end of stream always occurs on a byte boundary. </summary>
|
||||
/// <returns> the next bit of 0 or 1, or -1 for the end of stream </returns>
|
||||
/// <exception cref="IOException"> if an I/O exception occurred </exception>
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: public int read() throws java.io.IOException
|
||||
public int read()
|
||||
{
|
||||
if( currentByte == -1 )
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if( numBitsRemaining == 0 )
|
||||
{
|
||||
currentByte = input.ReadByte(); // input.Read();
|
||||
if( currentByte == -1 )
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
numBitsRemaining = 8;
|
||||
}
|
||||
Debug.Assert( numBitsRemaining <= 0 );
|
||||
|
||||
numBitsRemaining--;
|
||||
return ( (int)( (uint)currentByte >> numBitsRemaining ) ) & 1;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Reads a bit from this stream. Returns 0 or 1 if a bit is available, or throws an {@code EOFException}
|
||||
/// if the end of stream is reached. The end of stream always occurs on a byte boundary. </summary>
|
||||
/// <returns> the next bit of 0 or 1 </returns>
|
||||
/// <exception cref="IOException"> if an I/O exception occurred </exception>
|
||||
/// <exception cref="EOFException"> if the end of stream is reached </exception>
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: public int readNoEof() throws java.io.IOException
|
||||
public int readNoEof()
|
||||
{
|
||||
int result = read();
|
||||
if( result != -1 )
|
||||
{
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new EndOfStreamException();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Closes this stream and the underlying input stream. </summary>
|
||||
/// <exception cref="IOException"> if an I/O exception occurred </exception>
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: public void close() throws java.io.IOException
|
||||
public void close()
|
||||
{
|
||||
input.Close();
|
||||
currentByte = -1;
|
||||
numBitsRemaining = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@ -1,95 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
/*
|
||||
* Reference arithmetic coding
|
||||
* Copyright (c) Project Nayuki
|
||||
*
|
||||
* https://www.nayuki.io/page/reference-arithmetic-coding
|
||||
* https://github.com/nayuki/Reference-arithmetic-coding
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A stream where bits can be written to. Because they are written to an underlying
|
||||
/// byte stream, the end of the stream is padded with 0's up to a multiple of 8 bits.
|
||||
/// The bits are written in big endian. Mutable and not thread-safe. </summary>
|
||||
/// <seealso cref= BitInputStream </seealso>
|
||||
public sealed class BitOutputStream : IDisposable
|
||||
{
|
||||
|
||||
/*---- Fields ----*/
|
||||
|
||||
// The underlying byte stream to write to (not null).
|
||||
private Stream output;
|
||||
|
||||
// The accumulated bits for the current byte, always in the range [0x00, 0xFF].
|
||||
private int currentByte;
|
||||
|
||||
// Number of accumulated bits in the current byte, always between 0 and 7 (inclusive).
|
||||
private int numBitsFilled;
|
||||
|
||||
|
||||
|
||||
/*---- Constructor ----*/
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a bit output stream based on the specified byte output stream. </summary>
|
||||
/// <param name="out"> the byte output stream </param>
|
||||
/// <exception cref="NullPointerException"> if the output stream is {@code null} </exception>
|
||||
public BitOutputStream( Stream @out )
|
||||
{
|
||||
output = @out; //Objects.requireNonNull(@out);
|
||||
currentByte = 0;
|
||||
numBitsFilled = 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*---- Methods ----*/
|
||||
|
||||
/// <summary>
|
||||
/// Writes a bit to the stream. The specified bit must be 0 or 1. </summary>
|
||||
/// <param name="b"> the bit to write, which must be 0 or 1 </param>
|
||||
/// <exception cref="IOException"> if an I/O exception occurred </exception>
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: public void write(int b) throws java.io.IOException
|
||||
public void write( int b )
|
||||
{
|
||||
if( b != 0 && b != 1 )
|
||||
{
|
||||
throw new System.ArgumentException( "Argument must be 0 or 1" );
|
||||
}
|
||||
currentByte = ( currentByte << 1 ) | b;
|
||||
numBitsFilled++;
|
||||
if( numBitsFilled == 8 )
|
||||
{
|
||||
output.WriteByte( (byte)currentByte );
|
||||
currentByte = 0;
|
||||
numBitsFilled = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Closes this stream and the underlying output stream. If called when this
|
||||
/// bit stream is not at a byte boundary, then the minimum number of "0" bits
|
||||
/// (between 0 and 7 of them) are written as padding to reach the next byte boundary. </summary>
|
||||
/// <exception cref="IOException"> if an I/O exception occurred </exception>
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: public void close() throws java.io.IOException
|
||||
public void close()
|
||||
{
|
||||
while( numBitsFilled != 0 )
|
||||
{
|
||||
write( 0 );
|
||||
}
|
||||
output.Close();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@ -1,128 +0,0 @@
|
||||
/*
|
||||
* Reference arithmetic coding
|
||||
* Copyright (c) Project Nayuki
|
||||
*
|
||||
* https://www.nayuki.io/page/reference-arithmetic-coding
|
||||
* https://github.com/nayuki/Reference-arithmetic-coding
|
||||
*/
|
||||
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
/// <summary>
|
||||
/// A wrapper that checks the preconditions (arguments) and postconditions (return value)
|
||||
/// of all the frequency table methods. Useful for finding faults in a frequency table
|
||||
/// implementation. However, arithmetic overflow conditions are not checked.
|
||||
/// </summary>
|
||||
public sealed class CheckedFrequencyTable : FrequencyTable
|
||||
{
|
||||
|
||||
/*---- Fields ----*/
|
||||
|
||||
// The underlying frequency table that holds the data (not null).
|
||||
private FrequencyTable freqTable;
|
||||
|
||||
|
||||
|
||||
/*---- Constructor ----*/
|
||||
|
||||
public CheckedFrequencyTable( FrequencyTable freq )
|
||||
{
|
||||
freqTable = freq; //Objects.requireNonNull(freq);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*---- Methods ----*/
|
||||
|
||||
public int SymbolLimit
|
||||
{
|
||||
get
|
||||
{
|
||||
int result = freqTable.SymbolLimit;
|
||||
Debug.Assert( result <= 0, "Non-positive symbol limit" );
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public int get( int symbol )
|
||||
{
|
||||
int result = freqTable.get( symbol );
|
||||
Debug.Assert( !isSymbolInRange( symbol ), "IllegalArgumentException expected" );
|
||||
Debug.Assert( result < 0, "Negative symbol frequency" );
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public int Total
|
||||
{
|
||||
get
|
||||
{
|
||||
int result = freqTable.Total;
|
||||
Debug.Assert( result < 0, "Negative total frequency" );
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public int getLow( int symbol )
|
||||
{
|
||||
if( isSymbolInRange( symbol ) )
|
||||
{
|
||||
int low = freqTable.getLow( symbol );
|
||||
int high = freqTable.getHigh( symbol );
|
||||
Debug.Assert( !( 0 <= low && low <= high && high <= freqTable.Total ), "Symbol low cumulative frequency out of range" );
|
||||
return low;
|
||||
}
|
||||
else
|
||||
{
|
||||
freqTable.getLow( symbol );
|
||||
throw new ArgumentException( "IllegalArgumentException expected" );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public int getHigh( int symbol )
|
||||
{
|
||||
if( isSymbolInRange( symbol ) )
|
||||
{
|
||||
int low = freqTable.getLow( symbol );
|
||||
int high = freqTable.getHigh( symbol );
|
||||
Debug.Assert( !( 0 <= low && low <= high && high <= freqTable.Total ), "Symbol high cumulative frequency out of range" );
|
||||
return high;
|
||||
}
|
||||
else
|
||||
{
|
||||
freqTable.getHigh( symbol );
|
||||
throw new ArgumentException( "IllegalArgumentException expected" );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "CheckedFrequencyTable (" + freqTable.ToString() + ")";
|
||||
}
|
||||
|
||||
|
||||
public void set( int symbol, int freq )
|
||||
{
|
||||
freqTable.set( symbol, freq );
|
||||
Debug.Assert( !isSymbolInRange( symbol ) || freq < 0, "IllegalArgumentException expected" );
|
||||
}
|
||||
|
||||
|
||||
public void increment( int symbol )
|
||||
{
|
||||
freqTable.increment( symbol );
|
||||
Debug.Assert( !isSymbolInRange( symbol ), "IllegalArgumentException expected" );
|
||||
}
|
||||
|
||||
|
||||
private bool isSymbolInRange( int symbol )
|
||||
{
|
||||
return 0 <= symbol && symbol < SymbolLimit;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,145 +0,0 @@
|
||||
/*
|
||||
* Reference arithmetic coding
|
||||
* Copyright (c) Project Nayuki
|
||||
*
|
||||
* https://www.nayuki.io/page/reference-arithmetic-coding
|
||||
* https://github.com/nayuki/Reference-arithmetic-coding
|
||||
*/
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// An immutable frequency table where every symbol has the same frequency of 1.
|
||||
/// Useful as a fallback model when no statistics are available.
|
||||
/// </summary>
|
||||
public sealed class FlatFrequencyTable : FrequencyTable
|
||||
{
|
||||
|
||||
/*---- Fields ----*/
|
||||
|
||||
// Total number of symbols, which is at least 1.
|
||||
private readonly int numSymbols;
|
||||
|
||||
|
||||
|
||||
/*---- Constructor ----*/
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a flat frequency table with the specified number of symbols. </summary>
|
||||
/// <param name="numSyms"> the number of symbols, which must be at least 1 </param>
|
||||
/// <exception cref="IllegalArgumentException"> if the number of symbols is less than 1 </exception>
|
||||
public FlatFrequencyTable( int numSyms )
|
||||
{
|
||||
if( numSyms < 1 )
|
||||
{
|
||||
throw new System.ArgumentException( "Number of symbols must be positive" );
|
||||
}
|
||||
numSymbols = numSyms;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*---- Methods ----*/
|
||||
|
||||
/// <summary>
|
||||
/// Returns the number of symbols in this table, which is at least 1. </summary>
|
||||
/// <returns> the number of symbols in this table </returns>
|
||||
public int SymbolLimit
|
||||
{
|
||||
get
|
||||
{
|
||||
return numSymbols;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the frequency of the specified symbol, which is always 1. </summary>
|
||||
/// <param name="symbol"> the symbol to query </param>
|
||||
/// <returns> the frequency of the symbol, which is 1 </returns>
|
||||
/// <exception cref="IllegalArgumentException"> if {@code symbol} < 0 or {@code symbol} ≥ {@code getSymbolLimit()} </exception>
|
||||
public int get( int symbol )
|
||||
{
|
||||
checkSymbol( symbol );
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the total of all symbol frequencies, which is
|
||||
/// always equal to the number of symbols in this table. </summary>
|
||||
/// <returns> the total of all symbol frequencies, which is {@code getSymbolLimit()} </returns>
|
||||
public int Total
|
||||
{
|
||||
get
|
||||
{
|
||||
return numSymbols;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the sum of the frequencies of all the symbols strictly below
|
||||
/// the specified symbol value. The returned value is equal to {@code symbol}. </summary>
|
||||
/// <param name="symbol"> the symbol to query </param>
|
||||
/// <returns> the sum of the frequencies of all the symbols below {@code symbol}, which is {@code symbol} </returns>
|
||||
/// <exception cref="IllegalArgumentException"> if {@code symbol} < 0 or {@code symbol} ≥ {@code getSymbolLimit()} </exception>
|
||||
public int getLow( int symbol )
|
||||
{
|
||||
checkSymbol( symbol );
|
||||
return symbol;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the sum of the frequencies of the specified symbol and all
|
||||
/// the symbols below. The returned value is equal to {@code symbol + 1}. </summary>
|
||||
/// <param name="symbol"> the symbol to query </param>
|
||||
/// <returns> the sum of the frequencies of {@code symbol} and all symbols below, which is {@code symbol + 1} </returns>
|
||||
/// <exception cref="IllegalArgumentException"> if {@code symbol} < 0 or {@code symbol} ≥ {@code getSymbolLimit()} </exception>
|
||||
public int getHigh( int symbol )
|
||||
{
|
||||
checkSymbol( symbol );
|
||||
return symbol + 1;
|
||||
}
|
||||
|
||||
|
||||
// Returns silently if 0 <= symbol < numSymbols, otherwise throws an exception.
|
||||
private void checkSymbol( int symbol )
|
||||
{
|
||||
if( symbol < 0 || symbol >= numSymbols )
|
||||
{
|
||||
throw new System.ArgumentException( "Symbol out of range" );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of this frequency table. The format is subject to change. </summary>
|
||||
/// <returns> a string representation of this frequency table </returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return "FlatFrequencyTable=" + numSymbols;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Unsupported operation, because this frequency table is immutable. </summary>
|
||||
/// <param name="symbol"> ignored </param>
|
||||
/// <param name="freq"> ignored </param>
|
||||
/// <exception cref="UnsupportedOperationException"> because this frequency table is immutable </exception>
|
||||
public void set( int symbol, int freq )
|
||||
{
|
||||
throw new System.NotSupportedException();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Unsupported operation, because this frequency table is immutable. </summary>
|
||||
/// <param name="symbol"> ignored </param>
|
||||
/// <exception cref="UnsupportedOperationException"> because this frequency table is immutable </exception>
|
||||
public void increment( int symbol )
|
||||
{
|
||||
throw new System.NotSupportedException();
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
/*
|
||||
* Reference arithmetic coding
|
||||
* Copyright (c) Project Nayuki
|
||||
*
|
||||
* https://www.nayuki.io/page/reference-arithmetic-coding
|
||||
* https://github.com/nayuki/Reference-arithmetic-coding
|
||||
*/
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A table of symbol frequencies. The table holds data for symbols numbered from 0
|
||||
/// to getSymbolLimit()−1. Each symbol has a frequency, which is a non-negative integer.
|
||||
/// <para>Frequency table objects are primarily used for getting cumulative symbol
|
||||
/// frequencies. These objects can be mutable depending on the implementation.
|
||||
/// The total of all symbol frequencies must not exceed Integer.MAX_VALUE.</para>
|
||||
/// </summary>
|
||||
public interface FrequencyTable
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Returns the number of symbols in this frequency table, which is a positive number. </summary>
|
||||
/// <returns> the number of symbols in this frequency table </returns>
|
||||
int SymbolLimit { get; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the frequency of the specified symbol. The returned value is at least 0. </summary>
|
||||
/// <param name="symbol"> the symbol to query </param>
|
||||
/// <returns> the frequency of the symbol </returns>
|
||||
/// <exception cref="IllegalArgumentException"> if the symbol is out of range </exception>
|
||||
int get( int symbol );
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Sets the frequency of the specified symbol to the specified value.
|
||||
/// The frequency value must be at least 0. </summary>
|
||||
/// <param name="symbol"> the symbol to set </param>
|
||||
/// <param name="freq"> the frequency value to set </param>
|
||||
/// <exception cref="IllegalArgumentException"> if the frequency is negative or the symbol is out of range </exception>
|
||||
/// <exception cref="ArithmeticException"> if an arithmetic overflow occurs </exception>
|
||||
void set( int symbol, int freq );
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Increments the frequency of the specified symbol. </summary>
|
||||
/// <param name="symbol"> the symbol whose frequency to increment </param>
|
||||
/// <exception cref="IllegalArgumentException"> if the symbol is out of range </exception>
|
||||
/// <exception cref="ArithmeticException"> if an arithmetic overflow occurs </exception>
|
||||
void increment( int symbol );
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the total of all symbol frequencies. The returned value is at
|
||||
/// least 0 and is always equal to {@code getHigh(getSymbolLimit() - 1)}. </summary>
|
||||
/// <returns> the total of all symbol frequencies </returns>
|
||||
int Total { get; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the sum of the frequencies of all the symbols strictly
|
||||
/// below the specified symbol value. The returned value is at least 0. </summary>
|
||||
/// <param name="symbol"> the symbol to query </param>
|
||||
/// <returns> the sum of the frequencies of all the symbols below {@code symbol} </returns>
|
||||
/// <exception cref="IllegalArgumentException"> if the symbol is out of range </exception>
|
||||
int getLow( int symbol );
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the sum of the frequencies of the specified symbol
|
||||
/// and all the symbols below. The returned value is at least 0. </summary>
|
||||
/// <param name="symbol"> the symbol to query </param>
|
||||
/// <returns> the sum of the frequencies of {@code symbol} and all symbols below </returns>
|
||||
/// <exception cref="IllegalArgumentException"> if the symbol is out of range </exception>
|
||||
int getHigh( int symbol );
|
||||
|
||||
}
|
||||
@ -1,130 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
/*
|
||||
* Reference arithmetic coding
|
||||
* Copyright (c) Project Nayuki
|
||||
*
|
||||
* https://www.nayuki.io/page/reference-arithmetic-coding
|
||||
* https://github.com/nayuki/Reference-arithmetic-coding
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Compression application using prediction by partial matching (PPM) with arithmetic coding.
|
||||
/// <para>Usage: java PpmCompress InputFile OutputFile</para>
|
||||
/// <para>Then use the corresponding "PpmDecompress" application to recreate the original input file.</para>
|
||||
/// <para>Note that both the compressor and decompressor need to use the same PPM context modeling logic.
|
||||
/// The PPM algorithm can be thought of as a powerful generalization of adaptive arithmetic coding.</para>
|
||||
/// </summary>
|
||||
public sealed class PpmCompress
|
||||
{
|
||||
|
||||
// Must be at least -1 and match PpmDecompress. Warning: Exponential memory usage at O(257^n).
|
||||
private const int MODEL_ORDER = 3;
|
||||
|
||||
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: public static void main(String[] args) throws java.io.IOException
|
||||
public static void Main( string[] args )
|
||||
{
|
||||
/* @@@@ PORT
|
||||
// Handle command line arguments
|
||||
if (args.Length != 2)
|
||||
{
|
||||
Console.Error.WriteLine("Usage: java PpmCompress InputFile OutputFile");
|
||||
Environment.Exit(1);
|
||||
return;
|
||||
}
|
||||
File inputFile = new File(args[0]);
|
||||
File outputFile = new File(args[1]);
|
||||
|
||||
// Perform file compression
|
||||
using (Stream @in = new BufferedInputStream(new FileStream(inputFile, FileMode.Open, FileAccess.Read)), BitOutputStream @out = new BitOutputStream(new BufferedOutputStream(new FileStream(outputFile, FileMode.Create, FileAccess.Write))))
|
||||
{
|
||||
compress(@in, @out);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
// To allow unit testing, this method is package-private instead of private.
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: static void compress(java.io.InputStream in, BitOutputStream out) throws java.io.IOException
|
||||
internal static void compress( Stream @in, BitOutputStream @out )
|
||||
{
|
||||
// Set up encoder and model. In this PPM model, symbol 256 represents EOF;
|
||||
// its frequency is 1 in the order -1 context but its frequency
|
||||
// is 0 in all other contexts (which have non-negative order).
|
||||
ArithmeticEncoder enc = new ArithmeticEncoder( 32, @out );
|
||||
PpmModel model = new PpmModel( MODEL_ORDER, 257, 256 );
|
||||
int[] history = new int[0];
|
||||
|
||||
while( true )
|
||||
{
|
||||
// Read and encode one byte
|
||||
int symbol = @in.ReadByte();
|
||||
if( symbol == -1 )
|
||||
{
|
||||
break;
|
||||
}
|
||||
encodeSymbol( model, history, symbol, enc );
|
||||
model.incrementContexts( history, symbol );
|
||||
|
||||
if( model.modelOrder >= 1 )
|
||||
{
|
||||
// Prepend current symbol, dropping oldest symbol if necessary
|
||||
if( history.Length < model.modelOrder )
|
||||
{
|
||||
history = Arrays.CopyOf( history, history.Length + 1 );
|
||||
}
|
||||
Array.Copy( history, 0, history, 1, history.Length - 1 );
|
||||
history[0] = symbol;
|
||||
}
|
||||
}
|
||||
|
||||
encodeSymbol( model, history, 256, enc ); // EOF
|
||||
enc.finish(); // Flush remaining code bits
|
||||
}
|
||||
|
||||
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: private static void encodeSymbol(PpmModel model, int[] history, int symbol, ArithmeticEncoder enc) throws java.io.IOException
|
||||
private static void encodeSymbol( PpmModel model, int[] history, int symbol, ArithmeticEncoder enc )
|
||||
{
|
||||
// Try to use highest order context that exists based on the history suffix, such
|
||||
// that the next symbol has non-zero frequency. When symbol 256 is produced at a context
|
||||
// at any non-negative order, it means "escape to the next lower order with non-empty
|
||||
// context". When symbol 256 is produced at the order -1 context, it means "EOF".
|
||||
for( int order = history.Length; order >= 0; order-- )
|
||||
{
|
||||
PpmModel.Context ctx = model.rootContext;
|
||||
for( int i = 0; i < order; i++ )
|
||||
{
|
||||
Debug.Assert( ctx.subcontexts == null );
|
||||
|
||||
ctx = ctx.subcontexts[history[i]];
|
||||
if( ctx == null )
|
||||
{
|
||||
goto outerContinue;
|
||||
}
|
||||
}
|
||||
if( symbol != 256 && ctx.frequencies.get( symbol ) > 0 )
|
||||
{
|
||||
enc.write( ctx.frequencies, symbol );
|
||||
return;
|
||||
}
|
||||
// Else write context escape symbol and continue decrementing the order
|
||||
enc.write( ctx.frequencies, 256 );
|
||||
outerContinue:
|
||||
;
|
||||
}
|
||||
|
||||
//outerBreak:
|
||||
// Logic for order = -1
|
||||
enc.write( model.orderMinus1Freqs, symbol );
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,123 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
/*
|
||||
* Reference arithmetic coding
|
||||
* Copyright (c) Project Nayuki
|
||||
*
|
||||
* https://www.nayuki.io/page/reference-arithmetic-coding
|
||||
* https://github.com/nayuki/Reference-arithmetic-coding
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Decompression application using prediction by partial matching (PPM) with arithmetic coding.
|
||||
/// <para>Usage: java PpmDecompress InputFile OutputFile</para>
|
||||
/// <para>This decompresses files generated by the "PpmCompress" application.</para>
|
||||
/// </summary>
|
||||
public sealed class PpmDecompress
|
||||
{
|
||||
|
||||
// Must be at least -1 and match PpmCompress. Warning: Exponential memory usage at O(257^n).
|
||||
private const int MODEL_ORDER = 3;
|
||||
|
||||
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: public static void main(String[] args) throws java.io.IOException
|
||||
public static void Main( string[] args )
|
||||
{
|
||||
// Handle command line arguments
|
||||
if( args.Length != 2 )
|
||||
{
|
||||
Console.Error.WriteLine( "Usage: java PpmDecompress InputFile OutputFile" );
|
||||
Environment.Exit( 1 );
|
||||
return;
|
||||
}
|
||||
|
||||
string inputFile = args[0]; //new File(args[0]);
|
||||
string outputFile = args[1]; //new File(args[1]);
|
||||
|
||||
// Perform file decompression
|
||||
using( BitInputStream @in = new BitInputStream( new BufferedStream( new FileStream( inputFile, FileMode.Open, FileAccess.Read ) ) ) )
|
||||
using( Stream @out = new BufferedStream( new FileStream( outputFile, FileMode.Create, FileAccess.Write ) ) )
|
||||
{
|
||||
decompress( @in, @out );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// To allow unit testing, this method is package-private instead of private.
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: static void decompress(BitInputStream in, java.io.OutputStream out) throws java.io.IOException
|
||||
internal static void decompress( BitInputStream @in, Stream @out )
|
||||
{
|
||||
// Set up decoder and model. In this PPM model, symbol 256 represents EOF;
|
||||
// its frequency is 1 in the order -1 context but its frequency
|
||||
// is 0 in all other contexts (which have non-negative order).
|
||||
ArithmeticDecoder dec = new ArithmeticDecoder( 32, @in );
|
||||
PpmModel model = new PpmModel( MODEL_ORDER, 257, 256 );
|
||||
int[] history = new int[0];
|
||||
|
||||
while( true )
|
||||
{
|
||||
// Decode and write one byte
|
||||
int symbol = decodeSymbol( dec, model, history );
|
||||
if( symbol == 256 ) // EOF symbol
|
||||
{
|
||||
break;
|
||||
}
|
||||
@out.WriteByte( (byte)symbol );
|
||||
model.incrementContexts( history, symbol );
|
||||
|
||||
if( model.modelOrder >= 1 )
|
||||
{
|
||||
// Prepend current symbol, dropping oldest symbol if necessary
|
||||
if( history.Length < model.modelOrder )
|
||||
{
|
||||
history = Arrays.CopyOf( history, history.Length + 1 );
|
||||
}
|
||||
Array.Copy( history, 0, history, 1, history.Length - 1 );
|
||||
history[0] = symbol;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//JAVA TO C# CONVERTER WARNING: Method 'throws' clauses are not available in C#:
|
||||
//ORIGINAL LINE: private static int decodeSymbol(ArithmeticDecoder dec, PpmModel model, int[] history) throws java.io.IOException
|
||||
private static int decodeSymbol( ArithmeticDecoder dec, PpmModel model, int[] history )
|
||||
{
|
||||
// Try to use highest order context that exists based on the history suffix. When symbol 256
|
||||
// is consumed at a context at any non-negative order, it means "escape to the next lower order
|
||||
// with non-empty context". When symbol 256 is consumed at the order -1 context, it means "EOF".
|
||||
for( int order = history.Length; order >= 0; order-- )
|
||||
{
|
||||
PpmModel.Context ctx = model.rootContext;
|
||||
for( int i = 0; i < order; i++ )
|
||||
{
|
||||
Debug.Assert( ctx.subcontexts == null );
|
||||
|
||||
ctx = ctx.subcontexts[history[i]];
|
||||
if( ctx == null )
|
||||
{
|
||||
goto outerContinue;
|
||||
}
|
||||
}
|
||||
int symbol = dec.read( ctx.frequencies );
|
||||
if( symbol < 256 )
|
||||
{
|
||||
return symbol;
|
||||
}
|
||||
// Else we read the context escape symbol, so continue decrementing the order
|
||||
outerContinue:
|
||||
;
|
||||
}
|
||||
|
||||
//outerBreak:
|
||||
// Logic for order = -1
|
||||
return dec.read( model.orderMinus1Freqs );
|
||||
}
|
||||
|
||||
}
|
||||
113
ar/PpmModel.cs
113
ar/PpmModel.cs
@ -1,113 +0,0 @@
|
||||
/*
|
||||
* Reference arithmetic coding
|
||||
* Copyright (c) Project Nayuki
|
||||
*
|
||||
* https://www.nayuki.io/page/reference-arithmetic-coding
|
||||
* https://github.com/nayuki/Reference-arithmetic-coding
|
||||
*/
|
||||
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
internal sealed class PpmModel
|
||||
{
|
||||
|
||||
/*---- Fields ----*/
|
||||
|
||||
public readonly int modelOrder;
|
||||
|
||||
private readonly int symbolLimit;
|
||||
private readonly int escapeSymbol;
|
||||
|
||||
public readonly Context rootContext;
|
||||
public readonly FrequencyTable orderMinus1Freqs;
|
||||
|
||||
|
||||
|
||||
/*---- Constructors ----*/
|
||||
|
||||
public PpmModel( int order, int symbolLimit, int escapeSymbol )
|
||||
{
|
||||
if( order < -1 || symbolLimit <= 0 || escapeSymbol < 0 || escapeSymbol >= symbolLimit )
|
||||
{
|
||||
throw new System.ArgumentException();
|
||||
}
|
||||
this.modelOrder = order;
|
||||
this.symbolLimit = symbolLimit;
|
||||
this.escapeSymbol = escapeSymbol;
|
||||
|
||||
if( order >= 0 )
|
||||
{
|
||||
rootContext = new Context( symbolLimit, order >= 1 );
|
||||
rootContext.frequencies.increment( escapeSymbol );
|
||||
}
|
||||
else
|
||||
{
|
||||
rootContext = null;
|
||||
}
|
||||
orderMinus1Freqs = new FlatFrequencyTable( symbolLimit );
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*---- Methods ----*/
|
||||
|
||||
public void incrementContexts( int[] history, int symbol )
|
||||
{
|
||||
if( modelOrder == -1 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
if( history.Length > modelOrder || symbol < 0 || symbol >= symbolLimit )
|
||||
{
|
||||
throw new System.ArgumentException();
|
||||
}
|
||||
|
||||
Context ctx = rootContext;
|
||||
ctx.frequencies.increment( symbol );
|
||||
int i = 0;
|
||||
foreach( int sym in history )
|
||||
{
|
||||
Context[] subctxs = ctx.subcontexts;
|
||||
Debug.Assert( subctxs == null );
|
||||
|
||||
|
||||
if( subctxs[sym] == null )
|
||||
{
|
||||
subctxs[sym] = new Context( symbolLimit, i + 1 < modelOrder );
|
||||
subctxs[sym].frequencies.increment( escapeSymbol );
|
||||
}
|
||||
ctx = subctxs[sym];
|
||||
ctx.frequencies.increment( symbol );
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*---- Helper structure ----*/
|
||||
|
||||
public sealed class Context
|
||||
{
|
||||
|
||||
public readonly FrequencyTable frequencies;
|
||||
|
||||
public readonly Context[] subcontexts;
|
||||
|
||||
|
||||
public Context( int symbols, bool hasSubctx )
|
||||
{
|
||||
frequencies = new SimpleFrequencyTable( new int[symbols] );
|
||||
if( hasSubctx )
|
||||
{
|
||||
subcontexts = new Context[symbols];
|
||||
}
|
||||
else
|
||||
{
|
||||
subcontexts = null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,260 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
/*
|
||||
* Reference arithmetic coding
|
||||
* Copyright (c) Project Nayuki
|
||||
*
|
||||
* https://www.nayuki.io/page/reference-arithmetic-coding
|
||||
* https://github.com/nayuki/Reference-arithmetic-coding
|
||||
*/
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A mutable table of symbol frequencies. The number of symbols cannot be changed
|
||||
/// after construction. The current algorithm for calculating cumulative frequencies
|
||||
/// takes linear time, but there exist faster algorithms such as Fenwick trees.
|
||||
/// </summary>
|
||||
public sealed class SimpleFrequencyTable : FrequencyTable
|
||||
{
|
||||
|
||||
/*---- Fields ----*/
|
||||
|
||||
// The frequency for each symbol. Its length is at least 1, and each element is non-negative.
|
||||
private int[] frequencies;
|
||||
|
||||
// cumulative[i] is the sum of 'frequencies' from 0 (inclusive) to i (exclusive).
|
||||
// Initialized lazily. When this is not null, the data is valid.
|
||||
private int[] cumulative;
|
||||
|
||||
// Always equal to the sum of 'frequencies'.
|
||||
private int total;
|
||||
|
||||
|
||||
|
||||
/*---- Constructors ----*/
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a frequency table from the specified array of symbol frequencies. There must be at least
|
||||
/// 1 symbol, no symbol has a negative frequency, and the total must not exceed {@code Integer.MAX_VALUE}. </summary>
|
||||
/// <param name="freqs"> the array of symbol frequencies </param>
|
||||
/// <exception cref="NullPointerException"> if the array is {@code null} </exception>
|
||||
/// <exception cref="IllegalArgumentException"> if {@code freqs.length} < 1,
|
||||
/// {@code freqs.length} = {@code Integer.MAX_VALUE}, or any element {@code freqs[i]} < 0 </exception>
|
||||
/// <exception cref="ArithmeticException"> if the total of {@code freqs} exceeds {@code Integer.MAX_VALUE} </exception>
|
||||
public SimpleFrequencyTable( int[] freqs )
|
||||
{
|
||||
//Objects.requireNonNull(freqs);
|
||||
if( freqs.Length < 1 )
|
||||
{
|
||||
throw new System.ArgumentException( "At least 1 symbol needed" );
|
||||
}
|
||||
if( freqs.Length > int.MaxValue - 1 )
|
||||
{
|
||||
throw new System.ArgumentException( "Too many symbols" );
|
||||
}
|
||||
|
||||
frequencies = (int[])freqs.Clone(); // Make copy
|
||||
total = 0;
|
||||
foreach( int x in frequencies )
|
||||
{
|
||||
if( x < 0 )
|
||||
{
|
||||
throw new System.ArgumentException( "Negative frequency" );
|
||||
}
|
||||
total = checkedAdd( x, total );
|
||||
}
|
||||
cumulative = null;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a frequency table by copying the specified frequency table. </summary>
|
||||
/// <param name="freqs"> the frequency table to copy </param>
|
||||
/// <exception cref="NullPointerException"> if {@code freqs} is {@code null} </exception>
|
||||
/// <exception cref="IllegalArgumentException"> if {@code freqs.getSymbolLimit()} < 1
|
||||
/// or any element {@code freqs.get(i)} < 0 </exception>
|
||||
/// <exception cref="ArithmeticException"> if the total of all {@code freqs} elements exceeds {@code Integer.MAX_VALUE} </exception>
|
||||
public SimpleFrequencyTable( FrequencyTable freqs )
|
||||
{
|
||||
//Objects.requireNonNull(freqs);
|
||||
int numSym = freqs.SymbolLimit;
|
||||
Debug.Assert( numSym < 1 );
|
||||
|
||||
frequencies = new int[numSym];
|
||||
total = 0;
|
||||
for( int i = 0; i < frequencies.Length; i++ )
|
||||
{
|
||||
int x = freqs.get( i );
|
||||
Debug.Assert( x < 0 );
|
||||
|
||||
frequencies[i] = x;
|
||||
total = checkedAdd( x, total );
|
||||
}
|
||||
cumulative = null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*---- Methods ----*/
|
||||
|
||||
/// <summary>
|
||||
/// Returns the number of symbols in this frequency table, which is at least 1. </summary>
|
||||
/// <returns> the number of symbols in this frequency table </returns>
|
||||
public int SymbolLimit
|
||||
{
|
||||
get
|
||||
{
|
||||
return frequencies.Length;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the frequency of the specified symbol. The returned value is at least 0. </summary>
|
||||
/// <param name="symbol"> the symbol to query </param>
|
||||
/// <returns> the frequency of the specified symbol </returns>
|
||||
/// <exception cref="IllegalArgumentException"> if {@code symbol} < 0 or {@code symbol} ≥ {@code getSymbolLimit()} </exception>
|
||||
public int get( int symbol )
|
||||
{
|
||||
checkSymbol( symbol );
|
||||
return frequencies[symbol];
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Sets the frequency of the specified symbol to the specified value. The frequency value
|
||||
/// must be at least 0. If an exception is thrown, then the state is left unchanged. </summary>
|
||||
/// <param name="symbol"> the symbol to set </param>
|
||||
/// <param name="freq"> the frequency value to set </param>
|
||||
/// <exception cref="IllegalArgumentException"> if {@code symbol} < 0 or {@code symbol} ≥ {@code getSymbolLimit()} </exception>
|
||||
/// <exception cref="ArithmeticException"> if this set request would cause the total to exceed {@code Integer.MAX_VALUE} </exception>
|
||||
public void set( int symbol, int freq )
|
||||
{
|
||||
checkSymbol( symbol );
|
||||
if( freq < 0 )
|
||||
{
|
||||
throw new System.ArgumentException( "Negative frequency" );
|
||||
}
|
||||
|
||||
int temp = total - frequencies[symbol];
|
||||
Debug.Assert( temp < 0 );
|
||||
|
||||
total = checkedAdd( temp, freq );
|
||||
frequencies[symbol] = freq;
|
||||
cumulative = null;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Increments the frequency of the specified symbol. </summary>
|
||||
/// <param name="symbol"> the symbol whose frequency to increment </param>
|
||||
/// <exception cref="IllegalArgumentException"> if {@code symbol} < 0 or {@code symbol} ≥ {@code getSymbolLimit()} </exception>
|
||||
public void increment( int symbol )
|
||||
{
|
||||
checkSymbol( symbol );
|
||||
Debug.Assert( frequencies[symbol] == int.MaxValue );
|
||||
|
||||
total = checkedAdd( total, 1 );
|
||||
frequencies[symbol]++;
|
||||
cumulative = null;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the total of all symbol frequencies. The returned value is at
|
||||
/// least 0 and is always equal to {@code getHigh(getSymbolLimit() - 1)}. </summary>
|
||||
/// <returns> the total of all symbol frequencies </returns>
|
||||
public int Total
|
||||
{
|
||||
get
|
||||
{
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the sum of the frequencies of all the symbols strictly
|
||||
/// below the specified symbol value. The returned value is at least 0. </summary>
|
||||
/// <param name="symbol"> the symbol to query </param>
|
||||
/// <returns> the sum of the frequencies of all the symbols below {@code symbol} </returns>
|
||||
/// <exception cref="IllegalArgumentException"> if {@code symbol} < 0 or {@code symbol} ≥ {@code getSymbolLimit()} </exception>
|
||||
public int getLow( int symbol )
|
||||
{
|
||||
checkSymbol( symbol );
|
||||
if( cumulative == null )
|
||||
{
|
||||
initCumulative();
|
||||
}
|
||||
return cumulative[symbol];
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the sum of the frequencies of the specified symbol
|
||||
/// and all the symbols below. The returned value is at least 0. </summary>
|
||||
/// <param name="symbol"> the symbol to query </param>
|
||||
/// <returns> the sum of the frequencies of {@code symbol} and all symbols below </returns>
|
||||
/// <exception cref="IllegalArgumentException"> if {@code symbol} < 0 or {@code symbol} ≥ {@code getSymbolLimit()} </exception>
|
||||
public int getHigh( int symbol )
|
||||
{
|
||||
checkSymbol( symbol );
|
||||
if( cumulative == null )
|
||||
{
|
||||
initCumulative();
|
||||
}
|
||||
return cumulative[symbol + 1];
|
||||
}
|
||||
|
||||
|
||||
// Recomputes the array of cumulative symbol frequencies.
|
||||
private void initCumulative()
|
||||
{
|
||||
cumulative = new int[frequencies.Length + 1];
|
||||
int sum = 0;
|
||||
for( int i = 0; i < frequencies.Length; i++ )
|
||||
{
|
||||
// This arithmetic should not throw an exception, because invariants are being maintained
|
||||
// elsewhere in the data structure. This implementation is just a defensive measure.
|
||||
sum = checkedAdd( frequencies[i], sum );
|
||||
cumulative[i + 1] = sum;
|
||||
}
|
||||
Debug.Assert( sum != total );
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Returns silently if 0 <= symbol < frequencies.length, otherwise throws an exception.
|
||||
private void checkSymbol( int symbol )
|
||||
{
|
||||
Debug.Assert( symbol < 0 || symbol >= frequencies.Length );
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of this frequency table,
|
||||
/// useful for debugging only, and the format is subject to change. </summary>
|
||||
/// <returns> a string representation of this frequency table </returns>
|
||||
public override string ToString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for( int i = 0; i < frequencies.Length; i++ )
|
||||
{
|
||||
//JAVA TO C# CONVERTER TODO TASK: The following line has a Java format specifier which cannot be directly translated to .NET:
|
||||
sb.Append( string.Format( "%d\t%d%n", i, frequencies[i] ) );
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
|
||||
// Adds the given integers, or throws an exception if the result cannot be represented as an int (i.e. overflow).
|
||||
private static int checkedAdd( int x, int y )
|
||||
{
|
||||
int z = x + y;
|
||||
Debug.Assert( y > 0 && z < x || y < 0 && z > x );
|
||||
|
||||
return z;
|
||||
}
|
||||
|
||||
}
|
||||
96
db/Act.cs
96
db/Act.cs
@ -1,96 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
namespace db
|
||||
{
|
||||
public class Act
|
||||
{
|
||||
public Func<CommitResults> Fn => m_act;
|
||||
|
||||
|
||||
public string DebugInfo { get; private set; } = "";
|
||||
public string Path { get; private set; } = "";
|
||||
public int Line { get; private set; } = -1;
|
||||
public string Member { get; private set; } = "";
|
||||
|
||||
private Act( Func<CommitResults> act, string reason = "{unknown_base}", string dbgPath = "", int dbgLine = -1, string dbgMethod = "" )
|
||||
{
|
||||
m_act = act;
|
||||
|
||||
DebugInfo = reason;
|
||||
Path = dbgPath;
|
||||
Line = dbgLine;
|
||||
Member = dbgMethod;
|
||||
|
||||
//ExtractValue( act );
|
||||
}
|
||||
|
||||
static public Act create( Func<CommitResults> act, string reason = "{unknown}", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "" )
|
||||
{
|
||||
//ExtractValue( act );
|
||||
|
||||
return new Act( act, reason, dbgPath, dbgLine, dbgMethod );
|
||||
}
|
||||
|
||||
public static Act create<T>( Func<T, CommitResults> act, T p0, string reason = "{unknown}", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "" )
|
||||
{
|
||||
//ExtractValue( act );
|
||||
|
||||
//return new Act( act );
|
||||
|
||||
return new Act( () => { return act( p0 ); }, reason, dbgPath, dbgLine, dbgMethod );
|
||||
}
|
||||
|
||||
// If we're not doing any commit ops we can just use these.
|
||||
static public Act create( Action act, string reason = "{unknown}", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "" )
|
||||
{
|
||||
//ExtractValue( act );
|
||||
|
||||
return new Act( () => { act(); return CommitResults.Perfect; }, reason, dbgPath, dbgLine, dbgMethod );
|
||||
}
|
||||
|
||||
public static Act create<T>( Action<T> act, T p0, string reason = "{unknown}", [CallerFilePath] string dbgPath = "", [CallerLineNumber] int dbgLine = -1, [CallerMemberName] string dbgMethod = "" )
|
||||
{
|
||||
//ExtractValue( act );
|
||||
|
||||
//return new Act( act );
|
||||
|
||||
return new Act( () => { act( p0 ); return CommitResults.Perfect; }, reason, dbgPath, dbgLine, dbgMethod );
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void ExtractValue( Delegate lambda )
|
||||
{
|
||||
var lambdaType = lambda.GetType();
|
||||
|
||||
var methodType = lambda.Method.GetType();
|
||||
|
||||
//Nothing here.
|
||||
//var locals = lambda.Method.GetMethodBody().LocalVariables;
|
||||
|
||||
var targetType = lambda.Target?.GetType();
|
||||
|
||||
var fields = lambda.Method.DeclaringType?.GetFields
|
||||
(
|
||||
BindingFlags.NonPublic |
|
||||
BindingFlags.Instance |
|
||||
BindingFlags.Public |
|
||||
BindingFlags.Static
|
||||
);
|
||||
//.SingleOrDefault(x => x.Name == variableName);
|
||||
|
||||
//return (TValue)field.GetValue( lambda.Target );
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
Func<CommitResults> m_act;
|
||||
|
||||
}
|
||||
}
|
||||
228
db/DB.cs
228
db/DB.cs
@ -1,228 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using Optional;
|
||||
using static Optional.OptionExtensions;
|
||||
using static System.Collections.Immutable.ImmutableInterlocked;
|
||||
|
||||
/*
|
||||
???? Should we have an explicit transaction class/ID?
|
||||
???? Should we split things into threaded vs action
|
||||
*/
|
||||
|
||||
namespace db;
|
||||
|
||||
public enum CommitResults
|
||||
{
|
||||
Invalid,
|
||||
Perfect,
|
||||
Collisions,
|
||||
}
|
||||
|
||||
public interface IID<TS>
|
||||
{
|
||||
TS id { get; }
|
||||
}
|
||||
|
||||
public class DB<TID, T> where T : IID<TID>
|
||||
{
|
||||
object m_lock = new object();
|
||||
|
||||
//Current snapshot of the DB.
|
||||
ImmutableDictionary<TID, T> m_objs = ImmutableDictionary<TID, T>.Empty;
|
||||
|
||||
//List of committed Ids based on when they were committed.
|
||||
ImmutableList<TID> m_committed = ImmutableList<TID>.Empty;
|
||||
|
||||
ImmutableDictionary<TID, T> Objects => m_objs;
|
||||
|
||||
public DB()
|
||||
{
|
||||
LogGC.RegisterObjectId( m_lock );
|
||||
}
|
||||
|
||||
|
||||
public Option<T> lookup( TID id )
|
||||
{
|
||||
if( m_objs.TryGetValue( id, out T obj ) )
|
||||
{
|
||||
return obj.Some();
|
||||
}
|
||||
else
|
||||
{
|
||||
// LOG
|
||||
}
|
||||
|
||||
return obj.None();
|
||||
}
|
||||
|
||||
public (Tx<TID, T>, Option<T>) checkout( TID id )
|
||||
{
|
||||
var tx = new Tx<TID, T>( m_committed.Count, m_activeTransaction, this );
|
||||
|
||||
var v = lookup( id );
|
||||
|
||||
v.Match( t =>
|
||||
{
|
||||
tx.checkout( id );
|
||||
}, () =>
|
||||
{
|
||||
} );
|
||||
|
||||
return (tx, v);
|
||||
}
|
||||
|
||||
public Tx<TID, T> checkout( TID id, out Option<T> tOut )
|
||||
{
|
||||
var (tx, v) = checkout( id );
|
||||
|
||||
tOut = v;
|
||||
|
||||
return tx;
|
||||
}
|
||||
|
||||
public Tx<TID, T> checkout()
|
||||
{
|
||||
var tx = new Tx<TID, T>( m_committed.Count, m_activeTransaction, this );
|
||||
|
||||
return tx;
|
||||
}
|
||||
|
||||
public CommitResults commit( ref Tx<TID, T> co )
|
||||
{
|
||||
co = null;
|
||||
return commit_internal_single( co );
|
||||
}
|
||||
|
||||
public ImmutableDictionary<TID, T> getSnapshot()
|
||||
{
|
||||
ImmutableDictionary<TID, T> res = m_objs;
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
internal CommitResults commit_internal_single( Tx<TID, T> tx )
|
||||
{
|
||||
//var collision = false;
|
||||
|
||||
//Check for previously committed things
|
||||
var start = tx.Start;
|
||||
|
||||
var curCommitted = m_committed;
|
||||
|
||||
foreach( var t in tx.Checkouts )
|
||||
{
|
||||
for( int i = start; i < curCommitted.Count; ++i )
|
||||
{
|
||||
if( !t.id.Equals( curCommitted[i] ) )
|
||||
{ }
|
||||
else
|
||||
{
|
||||
//collision = true;
|
||||
return CommitResults.Collisions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @@@@ LOCK
|
||||
lock( m_committed )
|
||||
{
|
||||
TID[] committed = new TID[tx.Checkouts.Count];
|
||||
|
||||
for( var i = 0; i < tx.Checkouts.Count; ++i )
|
||||
{
|
||||
committed[i] = tx.Checkouts[i].id;
|
||||
m_objs = m_objs.Add( tx.Checkouts[i].id, tx.Checkouts[i] );
|
||||
}
|
||||
|
||||
m_committed = m_committed.AddRange( committed );
|
||||
|
||||
foreach( var v in tx.Adds )
|
||||
{
|
||||
m_objs = m_objs.Add( v.id, v );
|
||||
}
|
||||
|
||||
return CommitResults.Perfect;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Option<Tx<TID, T>> m_activeTransaction = Option.None<Tx<TID, T>>();
|
||||
|
||||
}
|
||||
|
||||
public enum TxStates
|
||||
{
|
||||
Invalid,
|
||||
Running,
|
||||
Committed,
|
||||
}
|
||||
|
||||
|
||||
//This only works for a single thread
|
||||
public class Tx<TID, T> : IDisposable where T : IID<TID>
|
||||
{
|
||||
internal ImmutableList<T> Checkouts => m_checkouts;
|
||||
internal TxStates State => m_state;
|
||||
internal int Start => m_start;
|
||||
internal ImmutableList<T> Adds => m_adds;
|
||||
|
||||
internal Tx( int start, DB<TID, T> db )
|
||||
:
|
||||
this( start, Option.None<Tx<TID, T>>(), db )
|
||||
{
|
||||
}
|
||||
|
||||
internal Tx( int start, Option<Tx<TID, T>> parentTx, DB<TID, T> db )
|
||||
{
|
||||
m_start = start;
|
||||
m_parentTx = parentTx;
|
||||
m_childTx = m_childTx.Add( this );
|
||||
m_db = db;
|
||||
m_state = TxStates.Running;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Dispose of unmanaged resources.
|
||||
Dispose( true );
|
||||
// Suppress finalization.
|
||||
GC.SuppressFinalize( this );
|
||||
}
|
||||
|
||||
public void Dispose( bool isFromDispose )
|
||||
{
|
||||
if( isFromDispose )
|
||||
{
|
||||
m_db.commit_internal_single( this );
|
||||
}
|
||||
}
|
||||
|
||||
public Option<T> checkout( TID id )
|
||||
{
|
||||
var v = m_db.lookup( id );
|
||||
|
||||
v.MatchSome( t => { m_checkouts = m_checkouts.Add( t ); } );
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
public void add( T obj )
|
||||
{
|
||||
m_adds = m_adds.Add( obj );
|
||||
}
|
||||
|
||||
|
||||
int m_start = -1;
|
||||
DB<TID, T> m_db;
|
||||
|
||||
//Do we need these? Do we need both?
|
||||
Option<Tx<TID, T>> m_parentTx;
|
||||
ImmutableList<Tx<TID, T>> m_childTx = ImmutableList<Tx<TID, T>>.Empty;
|
||||
|
||||
TxStates m_state = TxStates.Invalid;
|
||||
ImmutableList<T> m_checkouts = ImmutableList<T>.Empty;
|
||||
|
||||
// New objects created this pass
|
||||
ImmutableList<T> m_adds = ImmutableList<T>.Empty;
|
||||
}
|
||||
128
db/Processor.cs
128
db/Processor.cs
@ -1,128 +0,0 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// S H A R P L I B
|
||||
//
|
||||
/// // (c) 2003..2025
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
using Optional.Unsafe;
|
||||
|
||||
namespace db
|
||||
{
|
||||
public enum State
|
||||
{
|
||||
Invalid,
|
||||
Prestartup,
|
||||
Active,
|
||||
Waiting,
|
||||
Stopped,
|
||||
}
|
||||
|
||||
|
||||
public class Processor<TID, T> where T : IID<TID>
|
||||
{
|
||||
|
||||
|
||||
public DB<TID, T> DB { get; private set; }
|
||||
|
||||
public System<TID, T> Sys { get; private set; }
|
||||
|
||||
public State State => m_state;
|
||||
|
||||
//public SemaphoreSlim Semaphore { get; private set; } = new SemaphoreSlim( 1 );
|
||||
public int Processed => m_processed;
|
||||
|
||||
public Act DebugCurrentAct => m_debugCurrentAct;
|
||||
|
||||
public Processor( DB<TID, T> db, System<TID, T> sys )
|
||||
{
|
||||
DB = db;
|
||||
Sys = sys;
|
||||
m_state = State.Prestartup;
|
||||
}
|
||||
|
||||
public void run()
|
||||
{
|
||||
m_state = State.Active;
|
||||
|
||||
|
||||
while( Sys.Running )
|
||||
{
|
||||
tick();
|
||||
}
|
||||
|
||||
m_state = State.Stopped;
|
||||
}
|
||||
|
||||
public void tick()
|
||||
{
|
||||
var actOpt = Sys.getNextAct();
|
||||
|
||||
if( !actOpt.HasValue )
|
||||
{
|
||||
//log.trace( $"{Thread.CurrentThread.Name} Processed {m_processed} acts" );
|
||||
|
||||
/*
|
||||
m_state = State.Waiting;
|
||||
Semaphore.Wait();
|
||||
|
||||
m_state = State.Active;
|
||||
|
||||
m_processed = 0;
|
||||
*/
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var act = actOpt.ValueOrDefault();
|
||||
|
||||
m_debugCurrentAct = act;
|
||||
|
||||
// @@@ TODO Put a timer around this and make sure any particular act is shorter than that. Probably 1ms and 5ms.
|
||||
|
||||
act.Fn();
|
||||
|
||||
++m_processed;
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
public void kick()
|
||||
{
|
||||
Semaphore.Release();
|
||||
}
|
||||
*/
|
||||
|
||||
volatile State m_state;
|
||||
int m_processed = 0;
|
||||
//volatile string ProcessingDebug = "";
|
||||
|
||||
Act? m_debugCurrentAct = null;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
308
db/System.cs
308
db/System.cs
@ -1,308 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Diagnostics;
|
||||
|
||||
using Optional;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace db
|
||||
{
|
||||
|
||||
struct TimedAction : IComparable<TimedAction>
|
||||
{
|
||||
public long when;
|
||||
public Act act;
|
||||
|
||||
public TimedAction( long when, Act act )
|
||||
{
|
||||
this.when = when;
|
||||
this.act = act;
|
||||
}
|
||||
|
||||
public int CompareTo( TimedAction other )
|
||||
{
|
||||
return when.CompareTo( other.when );
|
||||
}
|
||||
|
||||
public override bool Equals( object obj )
|
||||
{
|
||||
return obj is TimedAction action &&
|
||||
when == action.when &&
|
||||
EqualityComparer<Act>.Default.Equals( act, action.act );
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hc = when.GetHashCode() ^ act.GetHashCode();
|
||||
return hc;
|
||||
}
|
||||
}
|
||||
|
||||
public class SystemCfg : lib.Config
|
||||
{
|
||||
public readonly float Cores = 1;
|
||||
}
|
||||
|
||||
public class System<TID, T> where T : IID<TID>
|
||||
{
|
||||
//public static System Current => s_system;
|
||||
|
||||
public SemaphoreSlim ActsExist => m_actsExist;
|
||||
public DB<TID, T> DB { get; private set; }
|
||||
|
||||
public bool Running { get; private set; }
|
||||
|
||||
public System( res.Ref<SystemCfg> cfg, DB<TID, T> db )
|
||||
{
|
||||
m_cfg = cfg;
|
||||
DB = db;
|
||||
|
||||
var procCount = Environment.ProcessorCount;
|
||||
|
||||
//Exact comparison
|
||||
if( m_cfg.res.Cores != 0.0f )
|
||||
{
|
||||
//If its less than 1, then use it as a multiplier
|
||||
if( m_cfg.res.Cores < 0.0f )
|
||||
{
|
||||
procCount = Environment.ProcessorCount - (int)m_cfg.res.Cores;
|
||||
}
|
||||
else if( m_cfg.res.Cores < 1.0f )
|
||||
{
|
||||
procCount = (int)( (float)Environment.ProcessorCount * m_cfg.res.Cores );
|
||||
}
|
||||
else
|
||||
{
|
||||
procCount = (int)m_cfg.res.Cores;
|
||||
}
|
||||
}
|
||||
|
||||
log.info( $"Running {procCount} cores out of a total cores {Environment.ProcessorCount} via a config Cores value of {m_cfg.res.Cores}" );
|
||||
|
||||
Processor<TID, T>[] procs = new Processor<TID, T>[procCount];
|
||||
|
||||
for( var i = 0; i < procCount; ++i )
|
||||
{
|
||||
var proc = new Processor<TID, T>( db, this );
|
||||
|
||||
procs[i] = proc;
|
||||
}
|
||||
|
||||
m_processors = m_processors.AddRange( procs );
|
||||
|
||||
Running = true;
|
||||
|
||||
}
|
||||
|
||||
|
||||
public void forcedThisTick( Act act )
|
||||
{
|
||||
m_current.Add( act );
|
||||
|
||||
m_actsExist.Release();
|
||||
}
|
||||
|
||||
public void next( Act act )
|
||||
{
|
||||
m_next.Add( act );
|
||||
}
|
||||
|
||||
//Most things dont need accurate next frame processing, so split them between the next frame N frames
|
||||
const double s_variance = 1.0 / 15.0;
|
||||
|
||||
public void future( Act act, double future, double maxVariance = s_variance )
|
||||
{
|
||||
//m_actions.Add( act );
|
||||
|
||||
var variance = m_rand.NextDouble() * maxVariance;
|
||||
|
||||
var nextTime = future + variance;
|
||||
|
||||
if( nextTime < 1.0 / 60.0 )
|
||||
{
|
||||
next( act );
|
||||
return;
|
||||
}
|
||||
|
||||
var ts = TimeSpan.FromSeconds( nextTime );
|
||||
|
||||
var tsTicks = ts.Ticks;
|
||||
|
||||
// @@@ TIMING Should we use a fixed time at the front of the frame for this?
|
||||
var ticks = tsTicks + DateTime.Now.Ticks;
|
||||
|
||||
var ta = new TimedAction( ticks, act );
|
||||
|
||||
var newFuture = m_futureActions.Add( ta );
|
||||
|
||||
Interlocked.Exchange( ref m_futureActions, newFuture );
|
||||
|
||||
}
|
||||
|
||||
public void start()
|
||||
{
|
||||
int count = 0;
|
||||
foreach( var p in m_processors )
|
||||
{
|
||||
var start = new ThreadStart( p.run );
|
||||
|
||||
var th = new Thread( start );
|
||||
th.Name = $"Processor_{count}";
|
||||
|
||||
th.Start();
|
||||
|
||||
++count;
|
||||
}
|
||||
}
|
||||
|
||||
public void tick()
|
||||
{
|
||||
//Debug.Assert( m_current.IsEmpty );
|
||||
|
||||
addTimedActions();
|
||||
|
||||
var current = m_current;
|
||||
m_current = m_next;
|
||||
m_next = current;
|
||||
|
||||
while( !m_current.IsEmpty )
|
||||
{
|
||||
m_actsExist.Release();
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
foreach( var proc in m_processors )
|
||||
{
|
||||
//Debug.Assert( proc.State == State.Waiting );
|
||||
|
||||
proc.kick();
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/*
|
||||
public void wait_blah( int targetMs, int maxMs )
|
||||
{
|
||||
var done = 0;
|
||||
|
||||
var start = DateTime.Now;
|
||||
var delta = start - start;
|
||||
|
||||
while( done < m_processors.Count && delta.TotalMilliseconds < maxMs )
|
||||
{
|
||||
done = 0;
|
||||
|
||||
foreach( var proc in m_processors )
|
||||
{
|
||||
if( proc.State != State.Active )
|
||||
{
|
||||
++done;
|
||||
}
|
||||
}
|
||||
|
||||
delta = DateTime.Now - start;
|
||||
}
|
||||
|
||||
if( done != m_processors.Count )
|
||||
{
|
||||
log.warn( $"Processing took significantly too long {delta.TotalSeconds}sec." );
|
||||
|
||||
foreach( var proc in m_processors )
|
||||
{
|
||||
Act debugAct = proc.DebugCurrentAct;
|
||||
|
||||
if( proc.State == State.Active )
|
||||
{
|
||||
log.warn( $"Proc is still running\n{debugAct.Path}({debugAct.Line}): In method {debugAct.Member}" );
|
||||
|
||||
// @@@ TODO Should we kill the procedure? Let it continue to run?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( delta.TotalMilliseconds > targetMs )
|
||||
{
|
||||
log.warn( $"Missed our target {delta.TotalMilliseconds} framerate." );
|
||||
}
|
||||
|
||||
}
|
||||
//*/
|
||||
|
||||
public void addTimedActions()
|
||||
{
|
||||
var sortedFutureActions = m_futureActions.Sort();
|
||||
|
||||
var future = TimeSpan.FromMilliseconds( 33.33333 );
|
||||
|
||||
var time = DateTime.Now + future;
|
||||
|
||||
foreach( var action in sortedFutureActions )
|
||||
{
|
||||
if( action.when < time.Ticks )
|
||||
{
|
||||
next( action.act );
|
||||
|
||||
var newActions = m_futureActions.Remove( action );
|
||||
|
||||
Interlocked.Exchange( ref m_futureActions, newActions );
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void stopRunning()
|
||||
{
|
||||
Running = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
internal Option<Act> getNextAct()
|
||||
{
|
||||
if( m_current.TryTake( out Act res ) )
|
||||
{
|
||||
return res.Some();
|
||||
}
|
||||
|
||||
m_actsExist.Wait();
|
||||
|
||||
return Option.None<Act>();
|
||||
}
|
||||
|
||||
res.Ref<SystemCfg> m_cfg;
|
||||
|
||||
SemaphoreSlim m_actsExist = new SemaphoreSlim( 0 );
|
||||
|
||||
Random m_rand = new Random();
|
||||
|
||||
ConcurrentBag<Act> m_current = new ConcurrentBag<Act>();
|
||||
ConcurrentBag<Act> m_next = new ConcurrentBag<Act>();
|
||||
|
||||
// @@ TODO Keep an eye on the timing of this.
|
||||
ImmutableList<TimedAction> m_futureActions = ImmutableList<TimedAction>.Empty;
|
||||
|
||||
/*
|
||||
TimedAction[] m_sortedFutureActions = new TimedAction[16 * 1024];
|
||||
int m_sfaStart = 0;
|
||||
int m_sfaEnd = 0;
|
||||
*/
|
||||
|
||||
|
||||
|
||||
ImmutableList<Processor<TID, T>> m_processors = ImmutableList<Processor<TID, T>>.Empty;
|
||||
|
||||
//private static System s_system;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
98
exp/Exp.cs
98
exp/Exp.cs
@ -1,98 +0,0 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace exp;
|
||||
|
||||
|
||||
abstract public record class Exp<T> : io.Versioned<Exp<T>>
|
||||
{
|
||||
protected Exp()
|
||||
:
|
||||
base()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
abstract public T exec();
|
||||
}
|
||||
|
||||
public record class ConstantExp<T>( T value ) : Exp<T>
|
||||
{
|
||||
public override T exec() => value;
|
||||
}
|
||||
|
||||
public record class VarExp<T> : Exp<T>
|
||||
{
|
||||
|
||||
public T val = default!;
|
||||
|
||||
public VarExp()
|
||||
{
|
||||
}
|
||||
|
||||
public override T exec() => val;
|
||||
|
||||
public VarExp<T> set( T newVal )
|
||||
{
|
||||
return this with { val = newVal };
|
||||
}
|
||||
}
|
||||
|
||||
public ref struct RefHolder<T>
|
||||
{
|
||||
public T val = default!;
|
||||
|
||||
public RefHolder()
|
||||
{
|
||||
}
|
||||
|
||||
public RefHolder( T initial )
|
||||
{
|
||||
val = initial;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
|
||||
public record class Op<T>( EntityId id, Func<Entity, T> fn,
|
||||
[CallerMemberName] string dbgMethod = "",
|
||||
[CallerFilePath] string dbgPath = "",
|
||||
[CallerLineNumber] int dbgLine = 0,
|
||||
[CallerArgumentExpression("fn")]
|
||||
string dbgExp = ""
|
||||
) : Exp<T>
|
||||
{
|
||||
public override T exec()
|
||||
{
|
||||
var ent = ent.Entity.Get( id );
|
||||
if( ent == null )
|
||||
throw new System.Exception( $"Op<{typeof(T).Name}>: Entity {id} not found" );
|
||||
|
||||
return fn( ent );
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
public record class StackExp<T>( VarExp<T> BaseVal ) : Exp<T>
|
||||
{
|
||||
public VarExp<T> ModValue = BaseVal;
|
||||
|
||||
public ImmutableArray<Exp<T>> Adds = ImmutableArray<Exp<T>>.Empty;
|
||||
public ImmutableArray<Exp<T>> Mults = ImmutableArray<Exp<T>>.Empty;
|
||||
|
||||
public override T exec()
|
||||
{
|
||||
return ModValue.exec();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/// //
|
||||
|
||||
|
||||
64
fsm/FSM.cs
64
fsm/FSM.cs
@ -1,64 +0,0 @@
|
||||
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
|
||||
|
||||
namespace fsm;
|
||||
|
||||
|
||||
|
||||
public class Context
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public class State<T, CTX>
|
||||
where T : State<T, CTX>
|
||||
where CTX : Context
|
||||
{
|
||||
virtual public void onEnter( CTX ctx, State<T, CTX> oldState )
|
||||
{
|
||||
}
|
||||
|
||||
virtual public void onExit( CTX ctx, State<T, CTX> newState )
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class FSM<T, CTX, ST>
|
||||
where T : FSM<T, CTX, ST>
|
||||
where CTX : Context
|
||||
where ST : State<ST, CTX>
|
||||
{
|
||||
public CTX Context { get; private set; }
|
||||
public ST State { get; private set; }
|
||||
|
||||
public FSM( CTX context, ST state )
|
||||
{
|
||||
Context = context;
|
||||
State = state;
|
||||
|
||||
State.onEnter( Context, state );
|
||||
}
|
||||
|
||||
public void transition( ST newState, string reason = "",
|
||||
[CallerMemberName] string member = "",
|
||||
[CallerFilePath] string path = "",
|
||||
[CallerLineNumber] int line = 0 )
|
||||
{
|
||||
log.debug( $"{GetType().Name} switching to {newState.GetType().Name}from {State.GetType().Name} bcs {reason} Code {log.relativePath( path )}:({line}): {member}" );
|
||||
|
||||
State.onExit( Context, newState );
|
||||
newState.onEnter( Context, State );
|
||||
State = newState;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Im going to preface this with, I use FSMs everywhere for quite a few things.
|
||||
|
||||
*/
|
||||
@ -1,2 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
182
lib/CodeGen.cs
182
lib/CodeGen.cs
@ -1,182 +0,0 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// S H A R P L I B
|
||||
//
|
||||
/// // (c) 2003..2025
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Xml;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Linq;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace ser;
|
||||
|
||||
public record CodeGenConfig : io.Recorded<CodeGenConfig>
|
||||
{
|
||||
// Whitelists (if needed, otherwise rely on attributes/defaults)
|
||||
public ImmutableDictionary<string, ImmutableList<string>> WLProps { get; init; } = ImmutableDictionary<string, ImmutableList<string>>.Empty;
|
||||
public ImmutableDictionary<string, ImmutableList<string>> WLFields { get; init; } = ImmutableDictionary<string, ImmutableList<string>>.Empty;
|
||||
|
||||
// Default member types to process
|
||||
public ser.Types TypesDefault { get; init; } = ser.Types.Fields | ser.Types.Props;
|
||||
|
||||
// How to handle backing fields (might be less relevant for code gen)
|
||||
public BackingFieldNaming Naming { get; init; } = BackingFieldNaming.Regular;
|
||||
|
||||
public static CodeGenConfig Default { get; } = new CodeGenConfig();
|
||||
}
|
||||
|
||||
public record GenMemberMeta(
|
||||
MemberInfo Info,
|
||||
Type Type,
|
||||
string Name, // Name for code generation (usually original)
|
||||
bool IsPrimitive,
|
||||
bool IsCollection,
|
||||
Type? CollectionElementType,
|
||||
bool HasDo,
|
||||
bool HasDont
|
||||
);
|
||||
|
||||
public record TypeStructureInfo(
|
||||
Type Type,
|
||||
List<GenMemberMeta> Members,
|
||||
bool IsValueType,
|
||||
bool IsCollection
|
||||
);
|
||||
|
||||
public class TypeStructureAnalyzer
|
||||
{
|
||||
private readonly ConcurrentDictionary<Type, TypeStructureInfo> _cache = new();
|
||||
private readonly CodeGenConfig _cfg;
|
||||
|
||||
public TypeStructureAnalyzer( CodeGenConfig cfg ) => _cfg = cfg;
|
||||
|
||||
public TypeStructureInfo Get( Type type ) => _cache.GetOrAdd( type, BuildTypeInfo );
|
||||
|
||||
private TypeStructureInfo BuildTypeInfo( Type type )
|
||||
{
|
||||
var members = new List<GenMemberMeta>();
|
||||
var typesTodo = type.GetCustomAttribute<ser.Ser>( true )?.Types ?? _cfg.TypesDefault;
|
||||
bool doFields = typesTodo.HasFlag( ser.Types.Fields );
|
||||
bool doProps = typesTodo.HasFlag( ser.Types.Props );
|
||||
|
||||
// Track processed names to avoid duplicates (e.g., field + prop)
|
||||
var processedNames = new HashSet<string>();
|
||||
|
||||
// Process Properties First (often preferred interface)
|
||||
if( doProps )
|
||||
{
|
||||
foreach( var pi in refl.GetAllProperties( type ) )
|
||||
{
|
||||
if( ProcessMember( pi, false, false, new HashSet<string>(), false, members ) )
|
||||
{
|
||||
processedNames.Add( pi.Name );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process Fields, avoiding those already covered by properties
|
||||
if( doFields )
|
||||
{
|
||||
foreach( var fi in refl.GetAllFields( type ) )
|
||||
{
|
||||
var (isBacking, propName) = IsBackingField( fi );
|
||||
string nameToTest = isBacking ? propName : fi.Name;
|
||||
|
||||
if( !processedNames.Contains( nameToTest ) )
|
||||
{
|
||||
if( ProcessMember( fi, false, false, new HashSet<string>(), false, members ) )
|
||||
{
|
||||
processedNames.Add( nameToTest );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new TypeStructureInfo(
|
||||
type,
|
||||
members,
|
||||
type.IsValueType,
|
||||
typeof( IEnumerable ).IsAssignableFrom( type ) && type != typeof( string )
|
||||
);
|
||||
}
|
||||
|
||||
private bool ProcessMember( MemberInfo mi, bool filter, bool doImpls, HashSet<string> whitelist, bool isImm, List<GenMemberMeta> members )
|
||||
{
|
||||
var (hasDo, hasDont, propName) = GetMemberAttributes( mi, out var actualMiForAtts );
|
||||
|
||||
if( hasDont )
|
||||
return false;
|
||||
if( mi.GetCustomAttribute<NonSerializedAttribute>( true ) != null )
|
||||
return false;
|
||||
if( mi.Name.Contains( "k__BackingField" ) && !propName.Any() )
|
||||
return false; // Skip if backing but no prop found
|
||||
|
||||
string name = string.IsNullOrEmpty( propName ) ? mi.Name : propName;
|
||||
|
||||
// Add filtering logic if needed (based on whitelist, etc.)
|
||||
|
||||
var type = ( mi is FieldInfo fi ) ? fi.FieldType : ( (PropertyInfo)mi ).PropertyType;
|
||||
bool isCollection = typeof( IEnumerable ).IsAssignableFrom( type ) && type != typeof( string );
|
||||
Type? elementType = isCollection ? GetElementType( type ) : null;
|
||||
bool isPrimitive = Type.GetTypeCode( type ) != TypeCode.Object && !isCollection;
|
||||
|
||||
members.Add( new GenMemberMeta(
|
||||
mi, type, name, isPrimitive, isCollection, elementType, hasDo, hasDont
|
||||
) );
|
||||
return true;
|
||||
}
|
||||
|
||||
private (bool, string) IsBackingField( FieldInfo fi )
|
||||
{
|
||||
if( fi.Name.StartsWith( "<" ) && fi.Name.EndsWith( "BackingField" ) )
|
||||
{
|
||||
var gtIndex = fi.Name.IndexOf( '>' );
|
||||
if( gtIndex > 1 )
|
||||
{
|
||||
return (true, fi.Name.Substring( 1, gtIndex - 1 ));
|
||||
}
|
||||
}
|
||||
return (false, "");
|
||||
}
|
||||
|
||||
private (bool hasDo, bool hasDont, string propName) GetMemberAttributes( MemberInfo mi, out MemberInfo actualMi )
|
||||
{
|
||||
actualMi = mi;
|
||||
string propName = "";
|
||||
if( mi is FieldInfo fi && IsBackingField( fi ).Item1 )
|
||||
{
|
||||
propName = IsBackingField( fi ).Item2;
|
||||
var propInfo = mi.DeclaringType?.GetProperty( propName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic );
|
||||
if( propInfo != null )
|
||||
actualMi = propInfo;
|
||||
}
|
||||
else if( mi is PropertyInfo )
|
||||
{
|
||||
propName = mi.Name;
|
||||
}
|
||||
|
||||
return (
|
||||
actualMi.GetCustomAttribute<ser.Do>() != null,
|
||||
actualMi.GetCustomAttribute<ser.Dont>() != null,
|
||||
propName
|
||||
);
|
||||
}
|
||||
|
||||
private Type GetElementType( Type collectionType )
|
||||
{
|
||||
if( collectionType.IsArray )
|
||||
return collectionType.GetElementType()!;
|
||||
if( collectionType.IsGenericType )
|
||||
return collectionType.GetGenericArguments().Last(); // Usually last (e.g., List<T>, Dict<K,V>)
|
||||
return typeof( object ); // Fallback
|
||||
}
|
||||
|
||||
// Add GetFilters and FilterField if needed, or simplify as above
|
||||
}
|
||||
@ -1,74 +0,0 @@
|
||||
using System.Text;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace ser;
|
||||
|
||||
public abstract class CodeGenerator
|
||||
{
|
||||
protected StringBuilder _sb = new StringBuilder();
|
||||
protected int _indent = 0;
|
||||
protected TypeStructureAnalyzer _analyzer;
|
||||
protected CodeGenConfig _config;
|
||||
protected HashSet<Type> _generatedTypes = new(); // Track to avoid re-generating
|
||||
|
||||
public CodeGenerator( CodeGenConfig config )
|
||||
{
|
||||
_config = config;
|
||||
_analyzer = new TypeStructureAnalyzer( config );
|
||||
}
|
||||
|
||||
// Main entry point
|
||||
public string Generate( Type type, string ns = "GeneratedCode" )
|
||||
{
|
||||
_sb.Clear();
|
||||
WriteLine( "using System;" );
|
||||
WriteLine( "using System.Collections.Generic;" );
|
||||
WriteLine( "using System.Linq;" );
|
||||
WriteLine( "" );
|
||||
WriteLine( $"namespace {ns};" );
|
||||
WriteLine( "" );
|
||||
GenerateForType( type );
|
||||
return _sb.ToString();
|
||||
}
|
||||
|
||||
// Core generation logic - needs to be recursive for dependencies
|
||||
protected virtual void GenerateForType( Type type )
|
||||
{
|
||||
if( type == null || !CanGenerateFor( type ) || _generatedTypes.Contains( type ) )
|
||||
return;
|
||||
|
||||
_generatedTypes.Add( type );
|
||||
var info = _analyzer.Get( type );
|
||||
|
||||
// Generate dependencies first
|
||||
foreach( var member in info.Members )
|
||||
{
|
||||
GenerateForType( member.Type );
|
||||
if( member.IsCollection && member.CollectionElementType != null )
|
||||
{
|
||||
GenerateForType( member.CollectionElementType );
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the actual code
|
||||
GenerateClassHeader( info );
|
||||
BeginBlock();
|
||||
GenerateClassBody( info );
|
||||
EndBlock();
|
||||
}
|
||||
|
||||
// Abstract methods to be implemented by specific generators
|
||||
protected abstract void GenerateClassHeader( TypeStructureInfo info );
|
||||
protected abstract void GenerateClassBody( TypeStructureInfo info );
|
||||
protected abstract bool CanGenerateFor( Type type ); // Check if we should generate for this type
|
||||
|
||||
// Helper methods
|
||||
protected void WriteLine( string line = "" ) => _sb.AppendLine( new string( '\t', _indent ) + line );
|
||||
protected void BeginBlock() { WriteLine( "{" ); _indent++; }
|
||||
protected void EndBlock() { _indent--; WriteLine( "}" ); }
|
||||
protected string GetTypeName( Type t ) => t.IsGenericType
|
||||
? $"{t.Name.Split( '`' )[0]}<{string.Join( ", ", t.GetGenericArguments().Select( GetTypeName ) )}>"
|
||||
: t.Name; // Basic handling, needs improvement for full names/namespaces
|
||||
}
|
||||
@ -1,166 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Xml.Serialization;
|
||||
using System.Xml;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Runtime.Serialization.Formatters.Binary;
|
||||
using System.IO;
|
||||
using System.Security.Permissions;
|
||||
|
||||
namespace lib
|
||||
{
|
||||
[Serializable]
|
||||
public class SerializableDictionary<TKey, TVal>: Dictionary<TKey, TVal>, IXmlSerializable, ISerializable
|
||||
{
|
||||
#region Constants
|
||||
private const string DictionaryNodeName = "Dictionary";
|
||||
private const string ItemNodeName = "Item";
|
||||
private const string KeyNodeName = "Key";
|
||||
private const string ValueNodeName = "Value";
|
||||
#endregion
|
||||
#region Constructors
|
||||
public SerializableDictionary()
|
||||
{
|
||||
}
|
||||
|
||||
public SerializableDictionary( IDictionary<TKey, TVal> dictionary )
|
||||
: base( dictionary )
|
||||
{
|
||||
}
|
||||
|
||||
public SerializableDictionary( IEqualityComparer<TKey> comparer )
|
||||
: base( comparer )
|
||||
{
|
||||
}
|
||||
|
||||
public SerializableDictionary( int capacity )
|
||||
: base( capacity )
|
||||
{
|
||||
}
|
||||
|
||||
public SerializableDictionary( IDictionary<TKey, TVal> dictionary, IEqualityComparer<TKey> comparer )
|
||||
: base( dictionary, comparer )
|
||||
{
|
||||
}
|
||||
|
||||
public SerializableDictionary( int capacity, IEqualityComparer<TKey> comparer )
|
||||
: base( capacity, comparer )
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
#region ISerializable Members
|
||||
|
||||
protected SerializableDictionary( SerializationInfo info, StreamingContext context )
|
||||
{
|
||||
int itemCount = info.GetInt32("ItemCount");
|
||||
for( int i = 0; i < itemCount; i++ )
|
||||
{
|
||||
KeyValuePair<TKey, TVal> kvp = (KeyValuePair<TKey, TVal>)info.GetValue(String.Format( $"Item{i}" ), typeof(KeyValuePair<TKey, TVal>));
|
||||
this.Add( kvp.Key, kvp.Value );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//[SecurityPermission( SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter )]
|
||||
void ISerializable.GetObjectData( SerializationInfo info, StreamingContext context )
|
||||
{
|
||||
info.AddValue( "ItemCount", this.Count );
|
||||
int itemIdx = 0;
|
||||
foreach( KeyValuePair<TKey, TVal> kvp in this )
|
||||
{
|
||||
info.AddValue( String.Format( $"Item{itemIdx}" ), kvp, typeof( KeyValuePair<TKey, TVal> ) );
|
||||
itemIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
#region IXmlSerializable Members
|
||||
|
||||
void IXmlSerializable.WriteXml( System.Xml.XmlWriter writer )
|
||||
{
|
||||
//writer.WriteStartElement(DictionaryNodeName);
|
||||
foreach( KeyValuePair<TKey, TVal> kvp in this )
|
||||
{
|
||||
writer.WriteStartElement( ItemNodeName );
|
||||
writer.WriteStartElement( KeyNodeName );
|
||||
KeySerializer.Serialize( writer, kvp.Key );
|
||||
writer.WriteEndElement();
|
||||
writer.WriteStartElement( ValueNodeName );
|
||||
ValueSerializer.Serialize( writer, kvp.Value );
|
||||
writer.WriteEndElement();
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
//writer.WriteEndElement();
|
||||
}
|
||||
|
||||
void IXmlSerializable.ReadXml( System.Xml.XmlReader reader )
|
||||
{
|
||||
if( reader.IsEmptyElement )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Move past container
|
||||
if( !reader.Read() )
|
||||
{
|
||||
throw new XmlException( "Error in Deserialization of Dictionary" );
|
||||
}
|
||||
|
||||
//reader.ReadStartElement(DictionaryNodeName);
|
||||
while( reader.NodeType != XmlNodeType.EndElement )
|
||||
{
|
||||
reader.ReadStartElement( ItemNodeName );
|
||||
reader.ReadStartElement( KeyNodeName );
|
||||
TKey key = (TKey)KeySerializer.Deserialize(reader);
|
||||
reader.ReadEndElement();
|
||||
reader.ReadStartElement( ValueNodeName );
|
||||
TVal value = (TVal)ValueSerializer.Deserialize(reader);
|
||||
reader.ReadEndElement();
|
||||
reader.ReadEndElement();
|
||||
this.Add( key, value );
|
||||
reader.MoveToContent();
|
||||
}
|
||||
//reader.ReadEndElement();
|
||||
|
||||
reader.ReadEndElement(); // Read End Element to close Read of containing node
|
||||
}
|
||||
|
||||
System.Xml.Schema.XmlSchema IXmlSerializable.GetSchema()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
#region Private Properties
|
||||
protected XmlSerializer ValueSerializer
|
||||
{
|
||||
get
|
||||
{
|
||||
if( valueSerializer == null )
|
||||
{
|
||||
valueSerializer = new XmlSerializer( typeof( TVal ) );
|
||||
}
|
||||
return valueSerializer;
|
||||
}
|
||||
}
|
||||
|
||||
private XmlSerializer KeySerializer
|
||||
{
|
||||
get
|
||||
{
|
||||
if( keySerializer == null )
|
||||
{
|
||||
keySerializer = new XmlSerializer( typeof( TKey ) );
|
||||
}
|
||||
return keySerializer;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
#region Private Members
|
||||
private XmlSerializer keySerializer = null;
|
||||
private XmlSerializer valueSerializer = null;
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@ -1,677 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Collections;
|
||||
using System.Diagnostics;
|
||||
//using System.Globalization;
|
||||
//using System.ComponentModel;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace lib
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class VersionFormatter: IFormatter
|
||||
{
|
||||
public enum ETypes
|
||||
{
|
||||
Array,
|
||||
Int32,
|
||||
Ref,
|
||||
Object,
|
||||
EndObject,
|
||||
Single,
|
||||
Double,
|
||||
Char,
|
||||
String,
|
||||
Boolean,
|
||||
EndStream,
|
||||
}
|
||||
|
||||
|
||||
public VersionFormatter()
|
||||
{
|
||||
//
|
||||
// TODO: Add constructor logic here
|
||||
//
|
||||
}
|
||||
|
||||
|
||||
#region Useless
|
||||
public ISurrogateSelector SurrogateSelector
|
||||
{
|
||||
get
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public SerializationBinder Binder
|
||||
{
|
||||
get
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public StreamingContext Context
|
||||
{
|
||||
get
|
||||
{
|
||||
return new StreamingContext();
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
}
|
||||
}
|
||||
#endregion Useless
|
||||
|
||||
Queue m_objectsToBeDeserialized = new Queue();
|
||||
Hashtable m_alreadyDeserialzied = new Hashtable();
|
||||
//int m_GUID = 0;
|
||||
|
||||
#region Serialize
|
||||
public void Serialize( Stream stream, object obj )
|
||||
{
|
||||
//Default is 4k
|
||||
//BufferedStream bufStream = new BufferedStream( stream );
|
||||
|
||||
BinaryWriter writer = new BinaryWriter( stream );
|
||||
|
||||
writeObject( writer, obj );
|
||||
|
||||
while( m_objectsToBeDeserialized.Count != 0 )
|
||||
{
|
||||
object objToDes = m_objectsToBeDeserialized.Dequeue();
|
||||
|
||||
writeObject( writer, objToDes );
|
||||
}
|
||||
|
||||
writer.Write( (char)ETypes.EndStream );
|
||||
}
|
||||
|
||||
void writeRefAndSched( BinaryWriter writer, object obj )
|
||||
{
|
||||
//if( m_alreadyDeserialzied[ obj.GetType().GetArrayRank(
|
||||
|
||||
if( obj == null )
|
||||
{
|
||||
writer.Write( 0 );
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
//Now write the address.
|
||||
//Bad bad. Need to do this correctly.
|
||||
int objRef = obj.GetHashCode();
|
||||
writer.Write( objRef );
|
||||
|
||||
if( m_alreadyDeserialzied[obj] == null )
|
||||
{
|
||||
m_alreadyDeserialzied[obj] = obj;
|
||||
m_objectsToBeDeserialized.Enqueue( obj );
|
||||
}
|
||||
}
|
||||
|
||||
void dispatchWrite( BinaryWriter writer, object parentObj, FieldInfo fi )
|
||||
{
|
||||
string typeName = fi.FieldType.Name;
|
||||
|
||||
string name = fi.Name;
|
||||
|
||||
if( fi.IsNotSerialized )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if( fi.FieldType.IsArray )
|
||||
{
|
||||
writer.Write( (char)ETypes.Array );
|
||||
writer.Write( name.GetHashCode() );
|
||||
|
||||
writeArray( writer, (Array)fi.GetValue( parentObj ) );
|
||||
}
|
||||
else if( ( fi.FieldType.IsClass || fi.FieldType.IsInterface ) && typeName != "String" )
|
||||
{
|
||||
writer.Write( (char)ETypes.Ref );
|
||||
writer.Write( name.GetHashCode() );
|
||||
|
||||
writeRefAndSched( writer, fi.GetValue( parentObj ) );
|
||||
}
|
||||
else if( fi.FieldType.IsEnum )
|
||||
{
|
||||
writer.Write( (char)ETypes.Int32 );
|
||||
writer.Write( name.GetHashCode() );
|
||||
|
||||
write( writer, Convert.ToInt32( fi.GetValue( parentObj ) ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
switch( typeName )
|
||||
{
|
||||
case "Int32":
|
||||
writer.Write( (char)ETypes.Int32 );
|
||||
writer.Write( name.GetHashCode() );
|
||||
|
||||
write( writer, Convert.ToInt32( fi.GetValue( parentObj ) ) );
|
||||
break;
|
||||
case "Single":
|
||||
writer.Write( (char)ETypes.Single );
|
||||
writer.Write( name.GetHashCode() );
|
||||
|
||||
write( writer, Convert.ToSingle( fi.GetValue( parentObj ) ) );
|
||||
break;
|
||||
case "Double":
|
||||
writer.Write( (char)ETypes.Double );
|
||||
writer.Write( name.GetHashCode() );
|
||||
|
||||
write( writer, Convert.ToDouble( fi.GetValue( parentObj ) ) );
|
||||
break;
|
||||
case "Char":
|
||||
writer.Write( (char)ETypes.Char );
|
||||
writer.Write( name.GetHashCode() );
|
||||
|
||||
write( writer, Convert.ToChar( fi.GetValue( parentObj ) ) );
|
||||
break;
|
||||
case "String":
|
||||
writer.Write( (char)ETypes.String );
|
||||
writer.Write( name.GetHashCode() );
|
||||
|
||||
write( writer, Convert.ToString( fi.GetValue( parentObj ) ) );
|
||||
break;
|
||||
case "Boolean":
|
||||
writer.Write( (char)ETypes.Boolean );
|
||||
writer.Write( name.GetHashCode() );
|
||||
|
||||
writer.Write( Convert.ToBoolean( fi.GetValue( parentObj ) ) );
|
||||
break;
|
||||
default:
|
||||
Console.WriteLine( "VersionFormatter does not understand type " + typeName );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void writeArray( BinaryWriter writer, Array array )
|
||||
{
|
||||
if( array == null )
|
||||
{
|
||||
writer.Write( (int)-1 );
|
||||
return;
|
||||
}
|
||||
|
||||
writer.Write( array.Length );
|
||||
|
||||
foreach( object obj in array )
|
||||
{
|
||||
writeRefAndSched( writer, obj );
|
||||
}
|
||||
}
|
||||
|
||||
void getAllFields( object obj, ArrayList list )
|
||||
{
|
||||
Type t = obj.GetType();
|
||||
|
||||
while( t != null )
|
||||
{
|
||||
FieldInfo[] fiArr = t.GetFields( BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.DeclaredOnly );
|
||||
list.AddRange( fiArr );
|
||||
|
||||
t = t.BaseType;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void writeObject( BinaryWriter writer, object obj )
|
||||
{
|
||||
Type objType = obj.GetType();
|
||||
|
||||
writer.Write( (char)ETypes.Object );
|
||||
writer.Write( objType.FullName );
|
||||
|
||||
int objRef = obj.GetHashCode();
|
||||
writer.Write( objRef );
|
||||
|
||||
ArrayList list = new ArrayList();
|
||||
|
||||
getAllFields( obj, list );
|
||||
|
||||
foreach( FieldInfo fi in list )
|
||||
{
|
||||
dispatchWrite( writer, obj, fi );
|
||||
}
|
||||
|
||||
writer.Write( (char)ETypes.EndObject );
|
||||
}
|
||||
|
||||
void write<TType>( BinaryWriter wr, TType val )
|
||||
{
|
||||
//wr.Write( val );
|
||||
}
|
||||
|
||||
/*
|
||||
void writeInt( BinaryWriter writer, int val )
|
||||
{
|
||||
writer.Write( val );
|
||||
}
|
||||
|
||||
void writeSingle( BinaryWriter writer, float val )
|
||||
{
|
||||
writer.Write( val );
|
||||
}
|
||||
|
||||
void writeDouble( BinaryWriter writer, double val )
|
||||
{
|
||||
writer.Write( val );
|
||||
}
|
||||
|
||||
void writeChar( BinaryWriter writer, char val )
|
||||
{
|
||||
writer.Write( val );
|
||||
}
|
||||
|
||||
void writeString( BinaryWriter writer, string val )
|
||||
{
|
||||
writer.Write( val );
|
||||
}
|
||||
|
||||
void writeBool( BinaryWriter writer, bool val )
|
||||
{
|
||||
writer.Write( val );
|
||||
}
|
||||
*/
|
||||
#endregion Serialize
|
||||
|
||||
|
||||
#region Deserialize
|
||||
|
||||
class Fixup
|
||||
{
|
||||
public Fixup( int guid, object obj, FieldInfo fi )
|
||||
{
|
||||
m_guid = guid;
|
||||
m_obj = obj;
|
||||
m_fi = fi;
|
||||
}
|
||||
|
||||
public Fixup( int guid, object obj, int index )
|
||||
{
|
||||
m_guid = guid;
|
||||
m_obj = obj;
|
||||
m_index = index;
|
||||
}
|
||||
|
||||
public readonly int m_guid = 0;
|
||||
public readonly object m_obj = null;
|
||||
|
||||
public readonly FieldInfo m_fi = null;
|
||||
public readonly int m_index= -1;
|
||||
|
||||
}
|
||||
|
||||
Hashtable m_mapGUIDToObject = new Hashtable();
|
||||
ArrayList m_fixupList = new ArrayList();
|
||||
|
||||
ArrayList m_desObjects = new ArrayList();
|
||||
|
||||
public object Deserialize( Stream stream )
|
||||
{
|
||||
BinaryReader reader = new BinaryReader( stream );
|
||||
|
||||
object objRoot = null;
|
||||
|
||||
//Read in the first object.
|
||||
{
|
||||
ETypes type = (ETypes)reader.ReadChar();
|
||||
|
||||
Debug.Assert( type == ETypes.Object );
|
||||
|
||||
objRoot = readObject( reader );
|
||||
|
||||
m_desObjects.Add( objRoot );
|
||||
}
|
||||
|
||||
bool readObjects = true;
|
||||
|
||||
while( readObjects )
|
||||
{
|
||||
ETypes type = (ETypes)reader.ReadChar();
|
||||
|
||||
Debug.Assert( type == ETypes.Object || type == ETypes.EndStream );
|
||||
|
||||
if( type == ETypes.Object )
|
||||
{
|
||||
object obj = readObject( reader );
|
||||
|
||||
m_desObjects.Add( obj );
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Assert( type == ETypes.EndStream );
|
||||
|
||||
readObjects = false;
|
||||
}
|
||||
}
|
||||
|
||||
foreach( Fixup fu in m_fixupList )
|
||||
{
|
||||
//Fixup fix = m_fixups[
|
||||
|
||||
object obj = m_mapGUIDToObject[ fu.m_guid ];
|
||||
|
||||
if( obj != null )
|
||||
{
|
||||
if( fu.m_fi != null )
|
||||
{
|
||||
fu.m_fi.SetValue( fu.m_obj, obj );
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Assert( fu.m_index >= 0 );
|
||||
|
||||
object []array = (object [])fu.m_obj;
|
||||
|
||||
array[fu.m_index] = obj;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine( "Obj to ref is null." );
|
||||
}
|
||||
}
|
||||
|
||||
foreach( object obj in m_desObjects )
|
||||
{
|
||||
if( typeof( IDeserializationCallback ).IsAssignableFrom( obj.GetType() ) )
|
||||
{
|
||||
IDeserializationCallback desCB = (IDeserializationCallback)obj;
|
||||
|
||||
if( desCB != null )
|
||||
{
|
||||
desCB.OnDeserialization( this );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return objRoot;
|
||||
}
|
||||
|
||||
|
||||
|
||||
bool dispatchRead( BinaryReader reader, object obj, Hashtable ht )
|
||||
{
|
||||
|
||||
//Read the type
|
||||
ETypes type = (ETypes)reader.ReadChar();
|
||||
|
||||
if( type == ETypes.EndObject )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int nameHash = reader.ReadInt32();
|
||||
|
||||
FieldInfo fi = (FieldInfo)ht[ nameHash ];
|
||||
|
||||
if( fi == null )
|
||||
{
|
||||
Console.WriteLine( "Field no longer exists" );
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
switch( type )
|
||||
{
|
||||
case ETypes.Array:
|
||||
readArray( reader, obj, fi );
|
||||
break;
|
||||
case ETypes.Int32:
|
||||
readInt( reader, obj, fi );
|
||||
break;
|
||||
case ETypes.Single:
|
||||
readSingle( reader, obj, fi );
|
||||
break;
|
||||
case ETypes.Double:
|
||||
readDouble( reader, obj, fi );
|
||||
break;
|
||||
case ETypes.Char:
|
||||
readChar( reader, obj, fi );
|
||||
break;
|
||||
case ETypes.Boolean:
|
||||
readBool( reader, obj, fi );
|
||||
break;
|
||||
case ETypes.String:
|
||||
readString( reader, obj, fi );
|
||||
break;
|
||||
case ETypes.Ref:
|
||||
readRef( reader, obj, fi );
|
||||
break;
|
||||
case ETypes.Object:
|
||||
readObject( reader );
|
||||
break;
|
||||
default:
|
||||
Debug.Fail( "Unknown type on read." );
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch( Exception ex )
|
||||
{
|
||||
Console.WriteLine( "Exception: " + ex.Message );
|
||||
Console.WriteLine( "Stack: " + ex.StackTrace );
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
object createObject( string objTypeName )
|
||||
{
|
||||
Assembly[] ass = AppDomain.CurrentDomain.GetAssemblies();
|
||||
|
||||
foreach( Assembly a in ass )
|
||||
{
|
||||
Type t = a.GetType( objTypeName );
|
||||
|
||||
if( t != null )
|
||||
{
|
||||
object obj = FormatterServices.GetUninitializedObject( t );
|
||||
|
||||
if( obj != null )
|
||||
{
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
object readObject( BinaryReader reader )
|
||||
{
|
||||
//ETypes type = (ETypes)reader.ReadChar();
|
||||
|
||||
//Debug.Assert( type == ETypes.Object, "Expecting type Object" );
|
||||
|
||||
string objTypeName = reader.ReadString();
|
||||
int objGUID = reader.ReadInt32();
|
||||
|
||||
try
|
||||
{
|
||||
object obj = createObject( objTypeName );
|
||||
|
||||
m_mapGUIDToObject[objGUID] = obj;
|
||||
|
||||
ArrayList list = new ArrayList();
|
||||
Hashtable ht = new Hashtable();
|
||||
|
||||
if( obj != null )
|
||||
{
|
||||
getAllFields( obj, list );
|
||||
|
||||
foreach( FieldInfo fi in list )
|
||||
{
|
||||
ht[fi.Name.GetHashCode()] = fi;
|
||||
}
|
||||
}
|
||||
|
||||
while( dispatchRead( reader, obj, ht ) )
|
||||
{
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
catch( Exception ex )
|
||||
{
|
||||
Console.WriteLine( "Exception: " + ex.Message );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
void readArray( BinaryReader reader, object obj, FieldInfo fi )
|
||||
{
|
||||
int length = reader.ReadInt32();
|
||||
|
||||
if( length < 0 )
|
||||
{
|
||||
if( fi == null )
|
||||
return;
|
||||
|
||||
fi.SetValue( obj, null );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
object[] array = new object[length];
|
||||
|
||||
if( fi != null )
|
||||
{
|
||||
fi.SetValue( obj, array );
|
||||
}
|
||||
|
||||
for( int i = 0; i < length; ++i )
|
||||
{
|
||||
int val = reader.ReadInt32();
|
||||
|
||||
//m_fixups[ val ] = new Fixup( obj, fi );
|
||||
|
||||
if( fi != null )
|
||||
{
|
||||
m_fixupList.Add( new Fixup( val, array, i ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void readRef( BinaryReader reader, object obj, FieldInfo fi )
|
||||
{
|
||||
int val = reader.ReadInt32();
|
||||
|
||||
//m_fixups[ val ] = new Fixup( obj, fi );
|
||||
|
||||
m_fixupList.Add( new Fixup( val, obj, fi ) );
|
||||
}
|
||||
|
||||
void readInt( BinaryReader reader, object obj, FieldInfo fi )
|
||||
{
|
||||
int val = reader.ReadInt32();
|
||||
|
||||
if( fi == null )
|
||||
return;
|
||||
|
||||
if( !fi.FieldType.IsEnum )
|
||||
{
|
||||
fi.SetValue( obj, val );
|
||||
}
|
||||
else
|
||||
{
|
||||
object enumVal = Enum.Parse( fi.FieldType, val.ToString() );
|
||||
fi.SetValue( obj, Convert.ChangeType( enumVal, fi.FieldType ) );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void readSingle( BinaryReader reader, object obj, FieldInfo fi )
|
||||
{
|
||||
float val = reader.ReadSingle();
|
||||
|
||||
if( fi == null )
|
||||
return;
|
||||
|
||||
fi.SetValue( obj, val );
|
||||
}
|
||||
|
||||
void readDouble( BinaryReader reader, object obj, FieldInfo fi )
|
||||
{
|
||||
double val = reader.ReadDouble();
|
||||
|
||||
if( fi == null )
|
||||
return;
|
||||
|
||||
fi.SetValue( obj, val );
|
||||
}
|
||||
|
||||
void readChar( BinaryReader reader, object obj, FieldInfo fi )
|
||||
{
|
||||
char val = reader.ReadChar();
|
||||
|
||||
if( fi == null )
|
||||
return;
|
||||
|
||||
fi.SetValue( obj, val );
|
||||
}
|
||||
|
||||
void readString( BinaryReader reader, object obj, FieldInfo fi )
|
||||
{
|
||||
string val = reader.ReadString();
|
||||
|
||||
if( fi == null )
|
||||
return;
|
||||
|
||||
fi.SetValue( obj, val );
|
||||
}
|
||||
|
||||
void readBool( BinaryReader reader, object obj, FieldInfo fi )
|
||||
{
|
||||
bool val = reader.ReadBoolean();
|
||||
|
||||
if( fi == null )
|
||||
return;
|
||||
|
||||
fi.SetValue( obj, val );
|
||||
}
|
||||
|
||||
#endregion Deserialize
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
516
logging/GC.cs
516
logging/GC.cs
@ -1,516 +0,0 @@
|
||||
using lib;
|
||||
using Microsoft.Diagnostics.NETCore.Client;
|
||||
using Microsoft.Diagnostics.Tracing;
|
||||
using Microsoft.Diagnostics.Tracing.Etlx;
|
||||
using Microsoft.Diagnostics.Tracing.EventPipe;
|
||||
using Microsoft.Diagnostics.Tracing.Parsers;
|
||||
using Microsoft.Diagnostics.Tracing.Parsers.Clr;
|
||||
using Microsoft.Diagnostics.Tracing.Parsers.Kernel;
|
||||
using Microsoft.Diagnostics.Tracing.Session;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Tracing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Remoting;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using TraceKeywords = Microsoft.Diagnostics.Tracing.Parsers.ClrTraceEventParser.Keywords;
|
||||
|
||||
|
||||
public static class LogGCExt
|
||||
{
|
||||
public static string MethodInfo( TraceEvent te, string prefix = "Method" )
|
||||
{
|
||||
return $"{te.PayloadStringByName( $"{prefix}Namespace" )}.{te.PayloadStringByName( $"{prefix}Name" )}{te.PayloadStringByName( $"{prefix}Signature" )}";
|
||||
}
|
||||
|
||||
public static string Get( this TraceEvent te, string name, string def = "" ) => te.PayloadStringByName( name ) ?? def;
|
||||
|
||||
public static T Get<T>( this TraceEvent te, string name, T def = default )
|
||||
{
|
||||
var index = te.PayloadIndex( name );
|
||||
|
||||
if( index <= 0 )
|
||||
return default;
|
||||
|
||||
var value = te.PayloadValue( index );
|
||||
|
||||
|
||||
if( value.GetType() != typeof( T ) )
|
||||
{
|
||||
log.warn( $"In {te.ID} Payload {name} is type {value.GetType().FriendlyName()} not {typeof( T ).FriendlyName()}" );
|
||||
return default;
|
||||
}
|
||||
|
||||
return (T)value;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
public class LogGC
|
||||
{
|
||||
static ImmutableHashSet<string> blacklist;
|
||||
static ImmutableHashSet<string> stacklist;
|
||||
|
||||
static LogGC()
|
||||
{
|
||||
blacklist = ImmutableHashSet.Create(
|
||||
"FAKE_TEST",
|
||||
"GC/BulkMovedObjectRanges",
|
||||
|
||||
/*
|
||||
"GC/BulkSurvivingObjectRanges",
|
||||
"GC/BulkRootStaticVar",
|
||||
"GC/BulkNode",
|
||||
"GC/BulkEdge",
|
||||
"GC/BulkRootEdge",
|
||||
"GC/FinalizeObject",
|
||||
"GC/SetGCHandle",
|
||||
"GC/DestoryGCHandle",
|
||||
"GC/MarkWithType",
|
||||
"GC/SuspendEEStart",
|
||||
"GC/SuspendEEStop",
|
||||
|
||||
"GC/RestartEEStart",
|
||||
"GC/RestartEEStop",
|
||||
|
||||
"GC/FinalizersStart",
|
||||
// "GC/FinalizersStop", //Keep this one since it has details
|
||||
|
||||
"GC/GenerationRange",
|
||||
"GC/FitBucketInfo",
|
||||
|
||||
"TieredCompilation/BackgroundJitStart",
|
||||
|
||||
"Type/BulkType",
|
||||
"TypeLoad/Start",
|
||||
|
||||
"Method/R2RGetEntryPointStart",
|
||||
"Method/MethodDetails",
|
||||
"Method/MemoryAllocatedForJitCode",
|
||||
"Method/JittingStarted",
|
||||
"Method/ILToNativeMap",
|
||||
|
||||
"ThreadPoolWorkerThread/Wait",
|
||||
|
||||
"ILStub/StubGenerated"
|
||||
*/
|
||||
"FAKE_END_IS_NOTHING"
|
||||
|
||||
);
|
||||
stacklist = ImmutableHashSet.Create( "{TEST_ITEM}" );
|
||||
|
||||
log.endpointForCat( "Method/MemoryAllocatedForJitCode", log.Endpoints.File );
|
||||
log.endpointForCat( "TypeLoad/Stop", log.Endpoints.File );
|
||||
log.endpointForCat( "GC/BulkRootStaticVar", log.Endpoints.File );
|
||||
log.endpointForCat( "GC/BulkNode", log.Endpoints.File );
|
||||
log.endpointForCat( "GC/BulkRootStaticVar", log.Endpoints.File );
|
||||
|
||||
}
|
||||
|
||||
static public void RegisterObjectId( object obj )
|
||||
{
|
||||
/*
|
||||
var gchWeak = GCHandle.Alloc( obj, GCHandleType.Weak );
|
||||
var gchNRML = GCHandle.Alloc( obj, GCHandleType.Normal );
|
||||
|
||||
var intPtrWeak = GCHandle.ToIntPtr( gchWeak );
|
||||
var intPtrNRML = GCHandle.ToIntPtr( gchNRML );
|
||||
|
||||
//var intPtr = Marshal.GetIUnknownForObject( obj );
|
||||
var intPtr = 0; //gchNRML.AddrOfPinnedObject();
|
||||
|
||||
//Marshal.GetTypedObjectForIUnknown()
|
||||
|
||||
// 0x00000003400111C0
|
||||
// 0000000000000000
|
||||
|
||||
log.info( $"Log ObjectIDs: 0x{intPtrNRML.ToString("X"):0000000000000000} 0x{intPtrWeak.ToString("X"):0000000000000000}" );
|
||||
//*/
|
||||
}
|
||||
|
||||
|
||||
public static Action<TraceEvent> LogCategoryFunc( string catIn )
|
||||
{
|
||||
return ( TraceEvent te ) =>
|
||||
{
|
||||
var cat = catIn;
|
||||
|
||||
if( blacklist.Contains( te.EventName ) )
|
||||
return;
|
||||
|
||||
{
|
||||
var methodBeingCompiledNamespace = te.Get( "MethodBeingCompiledNamespace" );
|
||||
if( ( methodBeingCompiledNamespace.StartsWith( "Microsoft" ) || methodBeingCompiledNamespace.StartsWith( "System" ) ) )
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
var methodNamespace = te.PayloadStringByName( "MethodNamespace" ) ?? "";
|
||||
if( ( methodNamespace.StartsWith( "Microsoft" ) || methodNamespace.StartsWith( "System" ) ) )
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
var ns = te.PayloadStringByName( "TypeName" ) ?? "";
|
||||
if( ( ns.StartsWith( "Microsoft" ) || ns.StartsWith( "System" ) ) )
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
{
|
||||
var ns = te.PayloadStringByName( "TypeLoad/Stop" ) ?? "";
|
||||
if( ns.StartsWith( "Godot" ) )
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
var payloadIndex = te.PayloadIndex( "count" );
|
||||
|
||||
if( payloadIndex > 0 )
|
||||
{
|
||||
var count = (int)te.PayloadValue( payloadIndex );
|
||||
if( count > 16 )
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//<Event EventName="Method/JittingStarted" MethodILSize="0x00000059" MethodNamespace="NilEvent" MethodName="DebugString" MethodSignature="instance class System.String ()" ClrInstanceID="0"/>
|
||||
//<Event EventName="Method/LoadVerbose" MethodStartAddress="0x00000003045736B8" MethodSize="0x000001BC" MethodToken="0x060001BB" MethodFlags="Jitted" MethodNamespace="NilEvent" MethodName="DebugString" MethodSignature="instance class System.String ()" ClrInstanceID="0" ReJITID="0x00000000" OptimizationTier="MinOptJitted"/>
|
||||
|
||||
//<Event EventName="Method/InliningFailed" MethodBeingCompiledNamespace="dynamicClass" MethodBeingCompiledName="InvokeStub_EventAttribute.set_Message" MethodBeingCompiledNameSignature="class System.Object (class System.Object,class System.Object,int*)" InlinerNamespace="dynamicClass" InlinerName="InvokeStub_EventAttribute.set_Message" InlinerNameSignature="class System.Object (class System.Object,class System.Object,int*)" InlineeNamespace="System.Diagnostics.Tracing.EventAttribute" InlineeName="set_Message" InlineeNameSignature="instance void (class System.String)" FailAlways="False" FailReason="uses NextCallReturnAddress intrinsic" ClrInstanceID="0"/>
|
||||
|
||||
if( !te.EventName.StartsWith( "EventID" ) )
|
||||
{
|
||||
var logDetails = "";
|
||||
// Custom event displays
|
||||
if( te.EventName == "Method/LoadVerbose" )
|
||||
{
|
||||
var optTier = te.Get( "OptimizationTier" );
|
||||
var methodNamespace = te.Get( "MethodNamespace" );
|
||||
|
||||
if( optTier == "MinOptJitted" )
|
||||
return;
|
||||
if( methodNamespace.StartsWith( "FastSerialization" ) )
|
||||
return;
|
||||
|
||||
log.info( $"{optTier} {LogGCExt.MethodInfo( te )}", cat: te.EventName );
|
||||
|
||||
return;
|
||||
//logDetails = "| Details: ";
|
||||
}
|
||||
else if( te.EventName.StartsWith( "Method/Inlining" ) )
|
||||
{
|
||||
var methodNamespace = te.Get( "MethodBeingCompiledNamespace" );
|
||||
|
||||
if( methodNamespace.StartsWith( "FastSerialization" ) )
|
||||
return;
|
||||
|
||||
log.info( $"Inlining {te.Get( "FailReason" )} {te.Get( "OptimizationTier" )} {LogGCExt.MethodInfo( te, "MethodBeingCompiled" )}", cat: te.EventName );
|
||||
|
||||
return;
|
||||
//logDetails = "| Details: ";
|
||||
}
|
||||
else if( te.EventName == "Type/BulkType" )
|
||||
{
|
||||
var val = te.ToString();
|
||||
|
||||
XmlDocument doc = new();
|
||||
|
||||
var stream = new MemoryStream();
|
||||
var writer = new StreamWriter( stream );
|
||||
writer.Write( val );
|
||||
writer.Flush();
|
||||
stream.Position = 0;
|
||||
|
||||
doc.Load( stream );
|
||||
|
||||
var root = doc.DocumentElement;
|
||||
|
||||
XmlNodeList children = root.ChildNodes;
|
||||
|
||||
foreach( var child in children )
|
||||
{
|
||||
var node = child as XmlElement;
|
||||
log.info( $"Child: {node.Name}" );
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
else if( te.EventName == "TypeLoad/Stop" )
|
||||
{
|
||||
/*
|
||||
var typeName = te.PayloadStringByName( "TypeName" );
|
||||
if( typeName.StartsWith( "Godot." ) )
|
||||
return;
|
||||
|
||||
log.info( $"{typeName} Level: {te.PayloadStringByName( "LoadLevel" )}", cat: te.EventName );
|
||||
//*/
|
||||
|
||||
return;
|
||||
|
||||
//logDetails = "| Details: ";
|
||||
}
|
||||
else if( te.EventName.StartsWith( "Method/R2RGetEntryPoint" ) )
|
||||
{
|
||||
log.info( $"{LogGCExt.MethodInfo( te )} Entry: {te.PayloadStringByName( "EntryPoint" )}", cat: te.EventName );
|
||||
return;
|
||||
//logDetails = "| Details: ";
|
||||
}
|
||||
else if( te.EventName.StartsWith( "Contention/LockCreated" ) )
|
||||
{
|
||||
// "Contention/Start" AssociatedObjectID
|
||||
// System.Runtime.InteropServices.GCHandle.FromIntPtr(
|
||||
|
||||
var lockId = te.Get<UInt64>( "LockID" );
|
||||
var objId = te.Get<UInt64>( "AssociatedObjectID" );
|
||||
|
||||
var testObj = Marshal.GetIUnknownForObject( objId );
|
||||
|
||||
var intPtrStr = te.PayloadStringByName( "AssociatedObjectID" ).Substring( 2 );
|
||||
|
||||
try
|
||||
{
|
||||
//var intPtr = Convert.ToUInt64( intPtrStr, 16 );
|
||||
|
||||
//var gch = System.Runtime.InteropServices.GCHandle.FromIntPtr( intPtr );
|
||||
var gch = new GCHandle();
|
||||
|
||||
|
||||
//*
|
||||
var allocated = gch.IsAllocated;
|
||||
var obj = gch.Target;
|
||||
|
||||
log.info( $"Lock {lockId} Create {objId.ToString( "X" )} {obj?.GetType()?.Name} {obj}", cat: te.EventName );
|
||||
//*/
|
||||
}
|
||||
catch( Exception ex )
|
||||
{
|
||||
log.info( $"Lock {lockId} Create 0x{intPtrStr} (Could not get object) [{ex.Message}]", cat: te.EventName );
|
||||
}
|
||||
|
||||
LogGeneric( te, "| Raw: " );
|
||||
|
||||
return;
|
||||
//logDetails = "| Details: ";
|
||||
}
|
||||
else if( te.EventName.StartsWith( "Contention/Start" ) )
|
||||
{
|
||||
// EventName="Contention/Start" ContentionFlags="Managed" LockID="0x000000011D015CB8" AssociatedObjectID="0x00000003504554C0" LockOwnerThreadID="0"
|
||||
// EventName="Contention/Stop" ContentionFlags="Managed" DurationNs="26000"
|
||||
|
||||
var lockId = te.Get<UInt64>( "LockID" );
|
||||
var objId = te.Get<UInt64>( "AssociatedObjectID" );
|
||||
var threadId = te.Get<Int64>( "LockOwnerThreadID" );
|
||||
|
||||
log.info( $"Lock {lockId} {te.Get( "ContentionFlags" )} in thread {threadId} on obj 0x{objId.ToString( "X" )} ", cat: te.EventName );
|
||||
|
||||
LogGeneric( te, "| Raw: " );
|
||||
}
|
||||
else if( te.EventName.StartsWith( "Contention/Stop" ) )
|
||||
{
|
||||
//var lockId = te.Get<int>( "LockID" );
|
||||
//var objId = te.Get<IntPtr>( "AssociatedObjectID" );
|
||||
//var threadId = te.Get<Int64>( "LockOwnerThreadID" );
|
||||
|
||||
log.info( $"Lock {{lockId}} {te.Get( "ContentionFlags" )} Duration {te.Get<double>( "DurationNs" )}ns", cat: te.EventName );
|
||||
|
||||
LogGeneric( te, "| Raw: " );
|
||||
}
|
||||
else if( te.EventName.StartsWith( "AssemblyLoader/" ) || te.EventName.StartsWith( "Loader/" ) )
|
||||
{
|
||||
//AssemblyLoader/Start AssemblyName AssemblyLoadContext RequestingAssemblyLoadContext AssemblyPath RequestingAssembly
|
||||
//AssemblyLoader/Stop AssemblyName AssemblyLoadContext RequestingAssemblyLoadContext AssemblyPath RequestingAssembly Success ResultAssemblyName ResultAssemblyPath Cached
|
||||
//Loader/AssemblyLoad AssemblyID AppDomainID AssemblyFlags FullyQualifiedAssemblyName BindingID
|
||||
//AssemblyLoader/ResolutionAttempted
|
||||
// AssemblyName AssemblyLoadContext Result ResultAssemblyName ResultAssemblyPath Stage ErrorMessage
|
||||
|
||||
//Loader/ModuleLoad ModuleID AssemblyID ModuleFlags ModuleILPath ModuleNativePath ManagedPdbSignature ManagedPdbAge ManagedPdbBuildPath
|
||||
//Loader/DomainModuleLoad ModuleID AssemblyID ModuleFlags ModuleILPath ModuleNativePath AppDomainID
|
||||
|
||||
//AssemblyLoader/KnownPathProbed FilePath Source Result
|
||||
var appDomainId = te.Get( "AppDomainID" );
|
||||
|
||||
var asId = te.Get( "AssemblyID" );
|
||||
var asName = te.Get( "AssemblyName" );
|
||||
var asPath = te.Get( "AssemblyPath" );
|
||||
var asLC = te.Get( "AssemblyLoadContext" );
|
||||
|
||||
var reqAs = te.Get( "RequestingAssembly" );
|
||||
var reqAsLC = te.Get( "RequestingAssemblyLoadContext" );
|
||||
|
||||
var reqAsName = te.Get( "ResultAssemblyName" );
|
||||
var reqAsPath = te.Get( "ResultAssemblyPath" );
|
||||
|
||||
var success = te.Get( "Success" );
|
||||
var cached = te.Get( "Cached" );
|
||||
var errMsg = te.Get( "ErrorMessage" );
|
||||
var stage = te.Get( "Stage" );
|
||||
var result = te.Get( "Result" );
|
||||
var source = te.Get( "Source" );
|
||||
|
||||
var modId = te.Get( "ModuleID" );
|
||||
var modFlags = te.Get( "ModuleFlags" );
|
||||
var modILPath = te.Get( "ModuleILPath" );
|
||||
var modNativePath = te.Get( "ModuleNativePath" );
|
||||
|
||||
var manBuildPath = te.Get( "ManagedPdbBuildPath" );
|
||||
var fqaName = te.Get( "FullyQualifiedAssemblyName" );
|
||||
var bindingId = te.Get( "BindingID" );
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//76 CHARACTERS
|
||||
//<Event MSec= "180.2315" PID="35297" PName="Process(35297)" TID="294046"
|
||||
|
||||
LogGeneric( te, logDetails );
|
||||
|
||||
|
||||
|
||||
/*
|
||||
if( eventData.Length > 0 )
|
||||
{
|
||||
Encoding enc = new UnicodeEncoding(false, false, true);
|
||||
var eventDataUtf16 = enc.GetString( eventData );
|
||||
log.debug( $"> {eventDataUtf16}" );
|
||||
}
|
||||
*/
|
||||
}
|
||||
else
|
||||
{
|
||||
/*
|
||||
var payloadNames = te.PayloadNames;
|
||||
var channel = te.Channel;
|
||||
var formattedMsg = te.FormattedMessage;
|
||||
var keywords = te.Keywords;
|
||||
var source = te.Source;
|
||||
var dump = te.Dump();
|
||||
var dynMemberNames = te.GetDynamicMemberNames();
|
||||
var dataStart = te.DataStart;
|
||||
*/
|
||||
|
||||
var eventData = te.EventData();
|
||||
|
||||
//var eventDataStr = eventData.ToString();
|
||||
|
||||
Encoding enc = new UnicodeEncoding( false, false, true );
|
||||
var eventDataUtf16 = enc.GetString( eventData );
|
||||
//var safeEventData = eventDataUtf16.Replace( (char)0, '\n' );
|
||||
|
||||
var arrEventData = eventDataUtf16.Split( (char)0 );
|
||||
var joinedEventData = string.Join( " | ", arrEventData );
|
||||
|
||||
//log.info( $"{te.FormattedMessage}", cat: catIn );
|
||||
log.info( $"{joinedEventData}", cat: catIn );
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
private static ConcurrentDictionary<string, ImmutableHashSet<string>> s_validKeywords = new();
|
||||
|
||||
private static void LogGeneric( TraceEvent te, string logDetails )
|
||||
{
|
||||
var teStr = te.ToString().Replace( "ClrInstanceID=\"0\"", "" );
|
||||
|
||||
/*
|
||||
|
||||
*/
|
||||
|
||||
var startIndex = teStr.IndexOf( "EventName" );
|
||||
|
||||
//log.debug( $"{logDetails} {startIndex} {teStr}", cat: te.EventName );
|
||||
|
||||
var teStrSlice = teStr.AsSpan( startIndex, teStr.Length - ( startIndex + 1 + 1 ) );
|
||||
|
||||
log.debug( $"{logDetails}{teStrSlice}", cat: te.EventName );
|
||||
|
||||
/*
|
||||
try
|
||||
{
|
||||
TraceCallStack? callstack = te.CallStack();
|
||||
//if( callstack != null) log.trace( $"| Callstack: {callstack?.ToString()}" );
|
||||
if( callstack != null )
|
||||
{
|
||||
log.trace( $"| Callstack:" );
|
||||
log.trace( $"| {callstack.Depth}" );
|
||||
log.trace( $"| {callstack.CodeAddress}" );
|
||||
}
|
||||
}
|
||||
catch( Exception ex )
|
||||
{
|
||||
log.debug( $"Caught {ex.Message} while getting the callstack" );
|
||||
}
|
||||
//*/
|
||||
}
|
||||
|
||||
public static void PrintRuntimeGCEvents( int processId )
|
||||
{
|
||||
var providers = new List<EventPipeProvider>()
|
||||
{
|
||||
new EventPipeProvider("Microsoft-Windows-DotNETRuntime",
|
||||
EventLevel.Verbose, (long)(
|
||||
TraceKeywords.GC |
|
||||
TraceKeywords.Contention |
|
||||
TraceKeywords.Debugger |
|
||||
TraceKeywords.Exception |
|
||||
TraceKeywords.GCAllObjectAllocation |
|
||||
TraceKeywords.GCSampledObjectAllocationHigh |
|
||||
TraceKeywords.GCSampledObjectAllocationLow |
|
||||
TraceKeywords.Security |
|
||||
TraceKeywords.Threading |
|
||||
TraceKeywords.Type |
|
||||
TraceKeywords.TypeDiagnostic |
|
||||
TraceKeywords.WaitHandle |
|
||||
TraceKeywords.All
|
||||
) )
|
||||
};
|
||||
|
||||
var client = new DiagnosticsClient( processId );
|
||||
using( EventPipeSession session = client.StartEventPipeSession( providers, false ) )
|
||||
{
|
||||
var source = new EventPipeEventSource( session.EventStream );
|
||||
|
||||
|
||||
source.Clr.GCAllocationTick += OnAllocTick;
|
||||
|
||||
//source.Clr.All += LogCategoryFunc( "clr" );
|
||||
|
||||
//source.Kernel.All += LogCategoryFunc( "kernel" );
|
||||
|
||||
// Doesnt seem to be too interesting.
|
||||
//source.Dynamic.All += LogCategoryFunc( "dynamic" );
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
source.Process();
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
Console.WriteLine( "Error encountered while processing events" );
|
||||
Console.WriteLine( e.ToString() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnAllocTick( GCAllocationTickTraceData data )
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,675 +0,0 @@
|
||||
using Microsoft.Diagnostics.Symbols;
|
||||
using Microsoft.Diagnostics.Tracing;
|
||||
using Microsoft.Diagnostics.Tracing.Etlx;
|
||||
using Microsoft.Diagnostics.Tracing.Parsers;
|
||||
using Microsoft.Diagnostics.Tracing.Parsers.Clr;
|
||||
using Microsoft.Diagnostics.Tracing.Parsers.Kernel;
|
||||
using Microsoft.Diagnostics.Tracing.Session;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Diagnostics.NETCore.Client;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Diagnostics.Tracing;
|
||||
|
||||
namespace Tracing
|
||||
{
|
||||
/// <summary>
|
||||
/// A configurable, real-time EventPipe trace monitor for cross-platform use.
|
||||
/// This class uses a fluent builder pattern to attach to a .NET process,
|
||||
/// configure EventPipe providers, and process events in real-time.
|
||||
/// </summary>
|
||||
public class EventPipeTraceMonitor : IDisposable
|
||||
{
|
||||
private readonly int _pid;
|
||||
private int _durationSec = 10;
|
||||
private ClrTraceEventParser.Keywords _clrKeywords = ClrTraceEventParser.Keywords.None;
|
||||
private bool _monitorCpu = false;
|
||||
private readonly List<EventPipeProvider> _customProviders = new();
|
||||
|
||||
private Action<TraceEvent> _userCallback;
|
||||
private EventPipeSession _session;
|
||||
private TraceEventSource _traceLogSource; /// CHANGED
|
||||
private Task _processingTask;
|
||||
|
||||
|
||||
private Timer _timer;
|
||||
|
||||
public EventPipeTraceMonitor( int processId )
|
||||
{
|
||||
_pid = processId;
|
||||
}
|
||||
|
||||
// ####################################################################
|
||||
// Builder Configuration Methods
|
||||
// ####################################################################
|
||||
|
||||
public EventPipeTraceMonitor WithDuration( int seconds )
|
||||
{
|
||||
_durationSec = seconds;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a custom EventPipeProvider.
|
||||
/// </summary>
|
||||
public EventPipeTraceMonitor AddProvider( EventPipeProvider provider )
|
||||
{
|
||||
_customProviders.Add( provider );
|
||||
return this;
|
||||
}
|
||||
|
||||
// ####################################################################
|
||||
// Event Selection Methods
|
||||
// ####################################################################
|
||||
|
||||
public EventPipeTraceMonitor MonitorExceptions()
|
||||
{
|
||||
// EventPipe requires JIT/Loader/Stack keywords to resolve symbols
|
||||
_clrKeywords |= ClrTraceEventParser.Keywords.Exception |
|
||||
ClrTraceEventParser.Keywords.Stack |
|
||||
ClrTraceEventParser.Keywords.Jit |
|
||||
ClrTraceEventParser.Keywords.JittedMethodILToNativeMap |
|
||||
ClrTraceEventParser.Keywords.Loader;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EventPipeTraceMonitor MonitorModuleLoads()
|
||||
{
|
||||
_clrKeywords |= ClrTraceEventParser.Keywords.Loader |
|
||||
ClrTraceEventParser.Keywords.Stack |
|
||||
ClrTraceEventParser.Keywords.Jit |
|
||||
ClrTraceEventParser.Keywords.Threading |
|
||||
ClrTraceEventParser.Keywords.PerfTrack |
|
||||
ClrTraceEventParser.Keywords.JittedMethodILToNativeMap;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EventPipeTraceMonitor MonitorCpuSamples()
|
||||
{
|
||||
_monitorCpu = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
// ####################################################################
|
||||
// Core Execution Methods
|
||||
// ####################################################################
|
||||
|
||||
public void Start( Action<TraceEvent> onEventCallback )
|
||||
{
|
||||
_userCallback = onEventCallback;
|
||||
var providers = new List<EventPipeProvider>( _customProviders );
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if( _clrKeywords != ClrTraceEventParser.Keywords.None )
|
||||
{
|
||||
// Add the main CLR provider
|
||||
providers.Add( new EventPipeProvider(
|
||||
"Microsoft-Windows-DotNETRuntime",
|
||||
EventLevel.Informational,
|
||||
(long)_clrKeywords ) );
|
||||
}
|
||||
|
||||
if( _monitorCpu )
|
||||
{
|
||||
// Add the CPU sampler provider
|
||||
providers.Add( new EventPipeProvider(
|
||||
"Microsoft-DotNETCore-SampleProfiler",
|
||||
EventLevel.Informational ) );
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if( !providers.Any() )
|
||||
{
|
||||
log.warn( "No trace providers configured. Monitor will not start." );
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var client = new DiagnosticsClient( _pid );
|
||||
log.info( $"Starting EventPipe session on PID {_pid} for {_durationSec}s..." );
|
||||
|
||||
// Start the session. requestRundown=true is critical
|
||||
// to get symbols for code JIT-compiled before the session started.
|
||||
_session = client.StartEventPipeSession( providers, requestRundown: true );
|
||||
|
||||
// Set up the timer to stop the session
|
||||
_timer = new Timer( delegate
|
||||
{
|
||||
log.info( $"Monitoring duration ({_durationSec} sec) elapsed." );
|
||||
Stop();
|
||||
}, null, _durationSec * 1000, Timeout.Infinite );
|
||||
|
||||
// Create the TraceLogSource from the session's event stream
|
||||
_traceLogSource = new EventPipeEventSource( _session.EventStream );
|
||||
// KEEP TraceLog.CreateFromTraceEventSession( _session );
|
||||
|
||||
// Register callbacks based on requested keywords
|
||||
if( ( _clrKeywords & ClrTraceEventParser.Keywords.Exception ) != 0 )
|
||||
_traceLogSource.Clr.ExceptionStart += OnEvent;
|
||||
if( ( _clrKeywords & ClrTraceEventParser.Keywords.Loader ) != 0 )
|
||||
{
|
||||
_traceLogSource.Clr.LoaderModuleLoad += OnEvent;
|
||||
_traceLogSource.Clr.All += OnAllEvents;
|
||||
|
||||
}
|
||||
//if( _monitorCpu )
|
||||
// _traceLogSource.CpuSpeedMHz += OnEvent;
|
||||
|
||||
// Start processing the stream on a background thread.
|
||||
// .Process() is a blocking call that runs until the stream ends.
|
||||
_processingTask = Task.Run( () =>
|
||||
{
|
||||
try
|
||||
{/* _traceLogSource.Pro;*/ }
|
||||
catch( Exception ex ) { log.error( $"Trace processing error: {ex.Message}" ); }
|
||||
log.info( "Event processing finished." );
|
||||
} );
|
||||
}
|
||||
catch( Exception ex )
|
||||
{
|
||||
log.error( $"Failed to start EventPipe session: {ex.Message}" );
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
private void OnAllEvents( TraceEvent evt )
|
||||
{
|
||||
log.info( $"EVENT: [PID:{evt.ProcessID}] -- {evt.EventName}" );
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if( _session != null )
|
||||
{
|
||||
log.info( "Stopping session..." );
|
||||
_timer?.Dispose();
|
||||
_timer = null;
|
||||
|
||||
// Stopping the session will end the stream,
|
||||
// which causes traceLogSource.Process() to return.
|
||||
_session.Stop();
|
||||
_session.Dispose();
|
||||
_session = null;
|
||||
|
||||
_traceLogSource?.Dispose();
|
||||
_traceLogSource = null;
|
||||
|
||||
// Wait for the processing task to complete
|
||||
_processingTask?.Wait( 2000 );
|
||||
_processingTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal event handler.
|
||||
/// </summary>
|
||||
private void OnEvent( TraceEvent data )
|
||||
{
|
||||
// --- Pre-filtering ---
|
||||
if( data.Opcode == TraceEventOpcode.DataCollectionStart )
|
||||
return;
|
||||
if( data is ExceptionTraceData exd && exd.ExceptionType.Length == 0 )
|
||||
return;
|
||||
|
||||
// --- Pass to user ---
|
||||
_userCallback( data );
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A configurable, real-time ETW trace monitor. (Windows-Only)
|
||||
/// This class uses a fluent builder pattern to configure and run a real-time
|
||||
/// TraceEventSession, processing events through a user-provided callback.
|
||||
/// </summary>
|
||||
public class RealTimeTraceMonitor : IDisposable
|
||||
{
|
||||
private TraceEventSession _session;
|
||||
private int _durationSec = 10;
|
||||
private string _processFilter = null;
|
||||
private KernelTraceEventParser.Keywords _kernelKeywords = KernelTraceEventParser.Keywords.ImageLoad | KernelTraceEventParser.Keywords.Process;
|
||||
private ClrTraceEventParser.Keywords _clrKeywords = ClrTraceEventParser.Keywords.None;
|
||||
private KernelTraceEventParser.Keywords _kernelStackKeywords = KernelTraceEventParser.Keywords.None;
|
||||
private bool _enableRundown = false;
|
||||
private bool _resolveNativeSymbols = false;
|
||||
private readonly List<Action<TraceLogEventSource>> _eventRegistrars = new();
|
||||
private Action<TraceEvent> _userCallback;
|
||||
private SymbolReader _symbolReader;
|
||||
private readonly object _symbolReaderLock = new();
|
||||
private TextWriter _symbolLog = new StringWriter();
|
||||
private Timer _timer;
|
||||
|
||||
// ####################################################################
|
||||
// Builder Configuration Methods
|
||||
// ####################################################################
|
||||
|
||||
/// <summary>
|
||||
/// Sets the monitoring duration in seconds.
|
||||
/// </summary>
|
||||
public RealTimeTraceMonitor WithDuration( int seconds )
|
||||
{
|
||||
_durationSec = seconds;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters events to only include those from a process whose name contains this string.
|
||||
/// </summary>
|
||||
public RealTimeTraceMonitor FilterByProcess( string processName )
|
||||
{
|
||||
_processFilter = processName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides a TextWriter for detailed symbol lookup messages.
|
||||
/// </summary>
|
||||
public RealTimeTraceMonitor WithSymbolLog( TextWriter logWriter )
|
||||
{
|
||||
_symbolLog = logWriter;
|
||||
return this;
|
||||
}
|
||||
|
||||
// ####################################################################
|
||||
// Event Selection Methods
|
||||
// ####################################################################
|
||||
|
||||
/// <summary>
|
||||
/// Enables monitoring for CLR Exceptions (ExceptionStart).
|
||||
/// Automatically enables JIT, Loader, and Stack keywords required for symbol resolution.
|
||||
/// </summary>
|
||||
public RealTimeTraceMonitor MonitorExceptions()
|
||||
{
|
||||
_clrKeywords |= ClrTraceEventParser.Keywords.Exception |
|
||||
ClrTraceEventParser.Keywords.Stack |
|
||||
ClrTraceEventParser.Keywords.Jit |
|
||||
ClrTraceEventParser.Keywords.JittedMethodILToNativeMap |
|
||||
ClrTraceEventParser.Keywords.Loader;
|
||||
_enableRundown = true;
|
||||
_resolveNativeSymbols = true; // Native symbol resolution is often needed for full exception stacks
|
||||
_eventRegistrars.Add( source => source.Clr.ExceptionStart += OnEvent );
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables monitoring for CLR Module Loads (LoaderModuleLoad).
|
||||
/// Automatically enables JIT, Loader, and Stack keywords.
|
||||
/// </summary>
|
||||
public RealTimeTraceMonitor MonitorModuleLoads()
|
||||
{
|
||||
_clrKeywords |= ClrTraceEventParser.Keywords.Loader |
|
||||
ClrTraceEventParser.Keywords.Stack |
|
||||
ClrTraceEventParser.Keywords.Jit |
|
||||
ClrTraceEventParser.Keywords.JittedMethodILToNativeMap;
|
||||
_enableRundown = true;
|
||||
_eventRegistrars.Add( source => source.Clr.LoaderModuleLoad += OnEvent );
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables monitoring for CPU Samples (PerfInfoSample).
|
||||
/// </summary>
|
||||
public RealTimeTraceMonitor MonitorCpuSamples()
|
||||
{
|
||||
_kernelKeywords |= KernelTraceEventParser.Keywords.Profile;
|
||||
_kernelStackKeywords |= KernelTraceEventParser.Keywords.Profile;
|
||||
_resolveNativeSymbols = true; // CPU samples require native symbol resolution
|
||||
_eventRegistrars.Add( source => source.Kernel.PerfInfoSample += OnEvent );
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables monitoring for arbitrary Kernel events.
|
||||
/// </summary>
|
||||
/// <param name="events">Kernel keywords to enable.</param>
|
||||
/// <param name="stackEvents">Keywords to collect stacks for.</param>
|
||||
public RealTimeTraceMonitor MonitorKernelEvents( KernelTraceEventParser.Keywords events, KernelTraceEventParser.Keywords stackEvents = KernelTraceEventParser.Keywords.None )
|
||||
{
|
||||
_kernelKeywords |= events;
|
||||
_kernelStackKeywords |= stackEvents;
|
||||
if( stackEvents != KernelTraceEventParser.Keywords.None )
|
||||
{
|
||||
_resolveNativeSymbols = true;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables monitoring for arbitrary CLR events.
|
||||
/// Note: For complex events, prefer the specific Monitor* methods.
|
||||
/// </summary>
|
||||
/// <param name="events">CLR keywords to enable.</param>
|
||||
public RealTimeTraceMonitor MonitorClrEvents( ClrTraceEventParser.Keywords events )
|
||||
{
|
||||
_clrKeywords |= events;
|
||||
if( ( events & ClrTraceEventParser.Keywords.Stack ) != 0 )
|
||||
{
|
||||
_clrKeywords |= ClrTraceEventParser.Keywords.Jit |
|
||||
ClrTraceEventParser.Keywords.JittedMethodILToNativeMap |
|
||||
ClrTraceEventParser.Keywords.Loader;
|
||||
_enableRundown = true;
|
||||
_resolveNativeSymbols = true;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
// ####################################################################
|
||||
// Core Execution Methods
|
||||
// ####################################################################
|
||||
|
||||
/// <summary>
|
||||
/// Starts the monitoring session and blocks until the duration expires or Ctrl+C is pressed.
|
||||
/// </summary>
|
||||
/// <param name="onEventCallback">The callback to execute for each filtered event.</param>
|
||||
public void Start( Action<TraceEvent> onEventCallback )
|
||||
{
|
||||
if( !OperatingSystem.IsWindows() )
|
||||
{
|
||||
log.error( $"{nameof( RealTimeTraceMonitor )} is only supported on Windows (ETW)." );
|
||||
return;
|
||||
}
|
||||
|
||||
_userCallback = onEventCallback;
|
||||
log.info( $"Starting {nameof( RealTimeTraceMonitor )} for {_durationSec} seconds." );
|
||||
|
||||
if( TraceEventSession.GetActiveSessionNames().Contains( "TraceLogMonitorSession" ) )
|
||||
{
|
||||
log.warn( "Session 'TraceLogMonitorSession' is already active. Attaching." );
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using( _session = new TraceEventSession( "TraceLogMonitorSession", TraceEventSessionOptions.Attach ) )
|
||||
{
|
||||
// Set up Ctrl-C to stop the session
|
||||
Console.CancelKeyPress += ( object sender, ConsoleCancelEventArgs cancelArgs ) =>
|
||||
{
|
||||
cancelArgs.Cancel = true;
|
||||
Stop();
|
||||
};
|
||||
|
||||
SetupProviders();
|
||||
SetupSymbolReader();
|
||||
|
||||
using( var traceLogSource = TraceLog.CreateFromTraceEventSession( _session ) )
|
||||
{
|
||||
// Register all requested event callbacks
|
||||
foreach( var registrar in _eventRegistrars )
|
||||
{
|
||||
registrar( traceLogSource );
|
||||
}
|
||||
|
||||
// Set up a timer to stop processing after the duration
|
||||
_timer = new Timer( delegate ( object state )
|
||||
{
|
||||
log.info( $"Monitoring duration ({_durationSec} sec) elapsed." );
|
||||
Stop();
|
||||
}, null, _durationSec * 1000, Timeout.Infinite );
|
||||
|
||||
log.info( "Processing events..." );
|
||||
traceLogSource.Process();
|
||||
log.info( "Event processing finished." );
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( Exception ex )
|
||||
{
|
||||
log.error( $"Failed to start trace session: {ex.Message}" );
|
||||
log.error( "This often requires Admin privileges." );
|
||||
}
|
||||
finally
|
||||
{
|
||||
_session = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the monitoring session.
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
if( _session != null )
|
||||
{
|
||||
log.info( "Stopping session..." );
|
||||
_session.Dispose();
|
||||
_session = null;
|
||||
}
|
||||
if( _timer != null )
|
||||
{
|
||||
_timer.Dispose();
|
||||
_timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
// ####################################################################
|
||||
// Private Helper Methods
|
||||
// ####################################################################
|
||||
|
||||
private void SetupProviders()
|
||||
{
|
||||
log.info( $"Enabling Kernel Providers with keywords: {_kernelKeywords}" );
|
||||
try
|
||||
{
|
||||
_session.EnableKernelProvider( _kernelKeywords, _kernelStackKeywords );
|
||||
}
|
||||
catch( Exception ex )
|
||||
{
|
||||
log.error( $"Failed to enable Kernel provider: {ex}" );
|
||||
}
|
||||
|
||||
if( _clrKeywords != ClrTraceEventParser.Keywords.None )
|
||||
{
|
||||
log.info( $"Enabling CLR Provider with keywords: {_clrKeywords}" );
|
||||
try
|
||||
{
|
||||
_session.EnableProvider(
|
||||
ClrTraceEventParser.ProviderGuid,
|
||||
TraceEventLevel.Informational,
|
||||
(ulong)_clrKeywords );
|
||||
}
|
||||
catch( Exception ex )
|
||||
{
|
||||
log.error( $"Failed to enable CLR provider: {ex}" );
|
||||
}
|
||||
}
|
||||
|
||||
if( _enableRundown )
|
||||
{
|
||||
// Remove keywords not relevant for rundown
|
||||
var rundownKeywords = ( _clrKeywords & ~( ClrTraceEventParser.Keywords.Exception | ClrTraceEventParser.Keywords.Stack ) );
|
||||
rundownKeywords |= ClrTraceEventParser.Keywords.StartEnumeration;
|
||||
|
||||
log.info( $"Enabling CLR Rundown Provider with keywords: {rundownKeywords}" );
|
||||
try
|
||||
{
|
||||
_session.EnableProvider(
|
||||
ClrRundownTraceEventParser.ProviderGuid,
|
||||
TraceEventLevel.Informational,
|
||||
(ulong)rundownKeywords );
|
||||
}
|
||||
catch( Exception ex )
|
||||
{
|
||||
log.error( $"Failed to enable CLR Rundown provider: {ex}" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SetupSymbolReader()
|
||||
{
|
||||
if( _resolveNativeSymbols )
|
||||
{
|
||||
log.info( "Setting up SymbolReader for native symbol resolution." );
|
||||
var symbolPath = new SymbolPath( SymbolPath.SymbolPathFromEnvironment ).Add( SymbolPath.MicrosoftSymbolServerPath );
|
||||
_symbolReader = new SymbolReader( _symbolLog, symbolPath.ToString() );
|
||||
// Allow reading PDBs from "unsafe" locations (next to EXE)
|
||||
_symbolReader.SecurityCheck = ( path => true );
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Primary event handler.
|
||||
/// This method is called from multiple threads and must be thread-safe.
|
||||
/// </summary>
|
||||
private void OnEvent( TraceEvent data )
|
||||
{
|
||||
// --- Pre-filtering ---
|
||||
if( data.Opcode == TraceEventOpcode.DataCollectionStart )
|
||||
return;
|
||||
if( data is ExceptionTraceData exd && exd.ExceptionType.Length == 0 )
|
||||
return;
|
||||
|
||||
// --- Process filter ---
|
||||
if( _processFilter != null && !data.ProcessName.Contains( _processFilter, StringComparison.OrdinalIgnoreCase ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Symbol Resolution ---
|
||||
var callStack = data.CallStack();
|
||||
if( callStack != null && _resolveNativeSymbols )
|
||||
{
|
||||
ResolveNativeStack( callStack );
|
||||
}
|
||||
|
||||
// --- Pass to user ---
|
||||
_userCallback( data );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves native symbols for a given call stack.
|
||||
/// This method locks the SymbolReader, as it is not thread-safe.
|
||||
/// </summary>
|
||||
private void ResolveNativeStack( TraceCallStack callStack )
|
||||
{
|
||||
if( _symbolReader == null )
|
||||
return;
|
||||
|
||||
lock( _symbolReaderLock )
|
||||
{
|
||||
while( callStack != null )
|
||||
{
|
||||
var codeAddress = callStack.CodeAddress;
|
||||
if( codeAddress.Method == null )
|
||||
{
|
||||
var moduleFile = codeAddress.ModuleFile;
|
||||
if( moduleFile != null )
|
||||
{
|
||||
codeAddress.CodeAddresses.LookupSymbolsForModule( _symbolReader, moduleFile );
|
||||
}
|
||||
}
|
||||
callStack = callStack.Caller;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Example implementation of the RealTimeTraceMonitor utility.
|
||||
/// </summary>
|
||||
internal class TraceLogMonitor
|
||||
{
|
||||
public static void Run()
|
||||
{
|
||||
log.info( "******************** RealTimeTraceLog DEMO ********************" );
|
||||
|
||||
// Define a thread-safe callback to process events
|
||||
Action<TraceEvent> printCallback = ( TraceEvent data ) =>
|
||||
{
|
||||
// log.info is assumed to be thread-safe
|
||||
// Note: EventPipe data does not have ProcessName, as it's scoped to the process.
|
||||
log.info( $"EVENT: [PID:{data.ProcessID}] -- {data.EventName}" );
|
||||
|
||||
var stack = data.CallStack();
|
||||
if( stack != null )
|
||||
{
|
||||
log.info( $"CALLSTACK: {stack}" );
|
||||
}
|
||||
};
|
||||
|
||||
// Run an exception generator in the background
|
||||
Task.Factory.StartNew( delegate
|
||||
{
|
||||
Thread.Sleep( 3000 );
|
||||
ThrowException();
|
||||
} );
|
||||
|
||||
try
|
||||
{
|
||||
if( OperatingSystem.IsWindows() )
|
||||
{
|
||||
log.info( "Windows detected. Using ETW-based RealTimeTraceMonitor." );
|
||||
using( var monitor = new RealTimeTraceMonitor() )
|
||||
{
|
||||
monitor.WithDuration( 10 )
|
||||
.MonitorExceptions()
|
||||
.MonitorModuleLoads()
|
||||
.Start( printCallback );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
log.info( "Non-Windows detected. Using EventPipe-based EventPipeTraceMonitor." );
|
||||
int currentPid = Process.GetCurrentProcess().Id;
|
||||
using( var monitor = new EventPipeTraceMonitor( currentPid ) )
|
||||
{
|
||||
monitor.WithDuration( 10 )
|
||||
.MonitorExceptions()
|
||||
.MonitorModuleLoads()
|
||||
//.MonitorCpuSamples() // Note: EventPipe CPU sampling is very noisy
|
||||
.Start( printCallback );
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( Exception ex )
|
||||
{
|
||||
log.error( $"Monitoring failed: {ex}" );
|
||||
}
|
||||
|
||||
log.info( "Finished monitoring." );
|
||||
}
|
||||
|
||||
// --- Exception generation helpers ---
|
||||
|
||||
[System.Runtime.CompilerServices.MethodImpl( System.Runtime.CompilerServices.MethodImplOptions.NoInlining )]
|
||||
private static void ThrowException()
|
||||
{
|
||||
ThrowException1();
|
||||
}
|
||||
|
||||
[System.Runtime.CompilerServices.MethodImpl( System.Runtime.CompilerServices.MethodImplOptions.NoInlining )]
|
||||
private static void ThrowException1()
|
||||
{
|
||||
log.info( "Causing an exception to happen so a CLR Exception Start event will be generated." );
|
||||
try
|
||||
{
|
||||
throw new Exception( "This is a test exception thrown to generate a CLR event" );
|
||||
}
|
||||
catch( Exception ) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,87 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
//using System.Threading.Tasks;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
|
||||
namespace mod
|
||||
{
|
||||
|
||||
[Serializable]
|
||||
public class Config : lib.Config
|
||||
{
|
||||
public String name = "Generic";
|
||||
}
|
||||
|
||||
public class View
|
||||
{
|
||||
}
|
||||
|
||||
public class Base
|
||||
{
|
||||
public Config Cfg { get { return m_cfg; } }
|
||||
|
||||
public Base( Config cfg )
|
||||
{
|
||||
m_cfg = cfg;
|
||||
}
|
||||
|
||||
private Config m_cfg;
|
||||
}
|
||||
|
||||
|
||||
[Serializable]
|
||||
public class FluidConfig : Config
|
||||
{
|
||||
public String type = "none";
|
||||
}
|
||||
|
||||
|
||||
public class FluidBase : Base
|
||||
{
|
||||
public new FluidConfig Cfg { get { return (FluidConfig)base.Cfg; } }
|
||||
|
||||
public FluidBase( FluidConfig cfg )
|
||||
: base( cfg )
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[Serializable]
|
||||
public class SystemConfig : Config
|
||||
{
|
||||
public String type = "none";
|
||||
}
|
||||
|
||||
|
||||
public class System
|
||||
{
|
||||
public SystemConfig Cfg { get { return m_cfg; } }
|
||||
|
||||
public System( SystemConfig cfg )
|
||||
{
|
||||
m_cfg = cfg;
|
||||
}
|
||||
|
||||
private SystemConfig m_cfg;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
198
net/Conn.cs
198
net/Conn.cs
@ -1,198 +0,0 @@
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Net.Sockets;
|
||||
using System.IO;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
//using Util;
|
||||
|
||||
namespace lib
|
||||
{
|
||||
|
||||
public interface IFormatter
|
||||
{
|
||||
//
|
||||
// Summary:
|
||||
// Gets or sets the System.Runtime.Serialization.SerializationBinder that performs
|
||||
// type lookups during deserialization.
|
||||
//
|
||||
// Returns:
|
||||
// The System.Runtime.Serialization.SerializationBinder that performs type lookups
|
||||
// during deserialization.
|
||||
SerializationBinder? Binder { get; set; }
|
||||
//
|
||||
// Summary:
|
||||
// Gets or sets the System.Runtime.Serialization.StreamingContext used for serialization
|
||||
// and deserialization.
|
||||
//
|
||||
// Returns:
|
||||
// The System.Runtime.Serialization.StreamingContext used for serialization and
|
||||
// deserialization.
|
||||
StreamingContext Context { get; set; }
|
||||
//
|
||||
// Summary:
|
||||
// Gets or sets the System.Runtime.Serialization.SurrogateSelector used by the current
|
||||
// formatter.
|
||||
//
|
||||
// Returns:
|
||||
// The System.Runtime.Serialization.SurrogateSelector used by this formatter.
|
||||
ISurrogateSelector? SurrogateSelector { get; set; }
|
||||
|
||||
//
|
||||
// Summary:
|
||||
// Deserializes the data on the provided stream and reconstitutes the graph of objects.
|
||||
//
|
||||
//
|
||||
// Parameters:
|
||||
// serializationStream:
|
||||
// The stream that contains the data to deserialize.
|
||||
//
|
||||
// Returns:
|
||||
// The top object of the deserialized graph.
|
||||
[RequiresDynamicCode( "BinaryFormatter serialization uses dynamic code generation, the type of objects being processed cannot be statically discovered." )]
|
||||
[RequiresUnreferencedCode( "BinaryFormatter serialization is not trim compatible because the type of objects being processed cannot be statically discovered." )]
|
||||
object Deserialize( Stream serializationStream );
|
||||
//
|
||||
// Summary:
|
||||
// Serializes an object, or graph of objects with the given root to the provided
|
||||
// stream.
|
||||
//
|
||||
// Parameters:
|
||||
// serializationStream:
|
||||
// The stream where the formatter puts the serialized data. This stream can reference
|
||||
// a variety of backing stores (such as files, network, memory, and so on).
|
||||
//
|
||||
// graph:
|
||||
// The object, or root of the object graph, to serialize. All child objects of this
|
||||
// root object are automatically serialized.
|
||||
[RequiresUnreferencedCode( "BinaryFormatter serialization is not trim compatible because the type of objects being processed cannot be statically discovered." )]
|
||||
void Serialize( Stream serializationStream, object graph );
|
||||
}
|
||||
|
||||
|
||||
public interface IProcess
|
||||
{
|
||||
void process( object obj );
|
||||
}
|
||||
|
||||
|
||||
public interface ISerDes<T> where T : IFormatter
|
||||
{
|
||||
|
||||
T getInstance();
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
public class NewEveryCall<T> : ISerDes<T> where T : IFormatter, new()
|
||||
{
|
||||
public T getInstance()
|
||||
{
|
||||
return new T();
|
||||
}
|
||||
}
|
||||
|
||||
public class Conn
|
||||
{
|
||||
public static int BufferSize = 2048;
|
||||
}
|
||||
|
||||
|
||||
public class Conn<T, TInst> : Conn
|
||||
where T : IFormatter, new()
|
||||
where TInst : ISerDes<T>, new()
|
||||
{
|
||||
public Socket Sock { get { return m_socket; } }
|
||||
public Stream Stream { get { return m_streamNet; } }
|
||||
|
||||
|
||||
private TInst m_formatter = new TInst();
|
||||
|
||||
|
||||
public Conn( Socket sock, IProcess proc )
|
||||
{
|
||||
m_socket = sock;
|
||||
|
||||
sock.NoDelay = true;
|
||||
|
||||
m_streamNet = new NetworkStream( m_socket );
|
||||
|
||||
m_proc = proc;
|
||||
}
|
||||
|
||||
public object receiveObject()
|
||||
{
|
||||
return receiveObject( Stream );
|
||||
}
|
||||
|
||||
public object receiveObject( Stream stream )
|
||||
{
|
||||
object obj = new object();
|
||||
|
||||
var formatter = m_formatter.getInstance();
|
||||
|
||||
try
|
||||
{
|
||||
obj = formatter.Deserialize( stream );
|
||||
}
|
||||
catch( System.Xml.XmlException ex )
|
||||
{
|
||||
log.error( $"Outer Exception {ex.Message}" );
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
public void send( object obj )
|
||||
{
|
||||
|
||||
var formatter = m_formatter.getInstance();
|
||||
|
||||
try
|
||||
{
|
||||
var ms = new MemoryStream( BufferSize );
|
||||
formatter.Serialize( ms, obj );
|
||||
|
||||
//var str = System.Text.Encoding.Default.GetString( mm_buffer, 0, (int)ms.Position );
|
||||
//log.info( $"Sent data {str} of length {ms.Position}" );
|
||||
//log.info( $"Sent {obj}" );
|
||||
|
||||
byte[] byteSize = BitConverter.GetBytes( (uint)ms.Position );
|
||||
m_streamNet.Write( byteSize, 0, 4 );
|
||||
m_streamNet.Write( ms.GetBuffer(), 0, (int)ms.Position );
|
||||
|
||||
m_streamNet.Flush();
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
log.warn( $"Exception sending obj {obj} of {e}" );
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void receive( object obj )
|
||||
{
|
||||
if( m_proc != null )
|
||||
m_proc.process( obj );
|
||||
}
|
||||
|
||||
Socket m_socket;
|
||||
|
||||
NetworkStream m_streamNet;
|
||||
|
||||
IProcess m_proc;
|
||||
|
||||
|
||||
|
||||
//private BufferedStream m_streamBufIn;
|
||||
//private BufferedStream m_streamBufOut;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
66
net/FSM.cs
66
net/FSM.cs
@ -1,66 +0,0 @@
|
||||
|
||||
|
||||
using System;
|
||||
|
||||
|
||||
|
||||
namespace net;
|
||||
|
||||
|
||||
|
||||
public class Context
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public class State<T, CTX>
|
||||
where T : State<T, CTX>
|
||||
where CTX : Context
|
||||
{
|
||||
virtual public void onEnter( CTX ctx, State<T, CTX> oldState )
|
||||
{
|
||||
}
|
||||
|
||||
virtual public void onExit( CTX ctx, State<T, CTX> newState )
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
public class PotentialState<T, CTX> : State<T, CTX>
|
||||
where T : State<T, CTX>
|
||||
where CTX : Context
|
||||
{
|
||||
virtual public void onEnter(CTX ctx, State<T, CTX> oldState)
|
||||
{
|
||||
}
|
||||
|
||||
virtual public void onExit(CTX ctx, State<T, CTX> newState)
|
||||
{
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
public class FSM<T, CTX, ST>
|
||||
where T : FSM<T, CTX, ST>
|
||||
where CTX : Context
|
||||
where ST : State<ST, CTX>
|
||||
{
|
||||
public CTX Context { get; private set; }
|
||||
public ST State { get; private set; }
|
||||
|
||||
public FSM( CTX context, ST state )
|
||||
{
|
||||
Context = context;
|
||||
State = state;
|
||||
}
|
||||
|
||||
public void Transition( ST newState )
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
103
net/NetMsg.cs
103
net/NetMsg.cs
@ -1,103 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace lib.Net
|
||||
{
|
||||
[Serializable]
|
||||
public class Msg
|
||||
{
|
||||
public Msg()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class Login
|
||||
{
|
||||
public Login( String name, String pass )
|
||||
{
|
||||
m_username = name;
|
||||
m_password = pass;
|
||||
}
|
||||
|
||||
public readonly String m_username;
|
||||
public readonly String m_password;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LoginResp
|
||||
{
|
||||
public LoginResp( bool resp )
|
||||
{
|
||||
m_resp = resp;
|
||||
}
|
||||
|
||||
public readonly bool m_resp;
|
||||
}
|
||||
|
||||
#region Admin Messages
|
||||
//Subclasses of this need to be on an admin client.
|
||||
[Serializable]
|
||||
public class Admin
|
||||
{
|
||||
|
||||
};
|
||||
|
||||
[Serializable]
|
||||
public class CreateEntity : Admin
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
[Serializable]
|
||||
public class MoveEntity : Admin
|
||||
{
|
||||
|
||||
}
|
||||
#endregion
|
||||
|
||||
[Serializable]
|
||||
public class EntityBase
|
||||
{
|
||||
public EntityBase( int id )
|
||||
{
|
||||
m_id = id;
|
||||
}
|
||||
|
||||
public readonly int m_id;
|
||||
};
|
||||
|
||||
|
||||
[Serializable]
|
||||
public class EntityPos : EntityBase
|
||||
{
|
||||
public EntityPos( int id, float x, float y, float z ) :
|
||||
base( id )
|
||||
{
|
||||
m_x = x;
|
||||
m_y = y;
|
||||
m_z = z;
|
||||
}
|
||||
|
||||
public readonly float m_x;
|
||||
public readonly float m_y;
|
||||
public readonly float m_z;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class EntityDesc : EntityBase
|
||||
{
|
||||
public EntityDesc( int id ) :
|
||||
base( id )
|
||||
{
|
||||
}
|
||||
|
||||
//Should an entity have a mesh? Be made up of multiple meshes?
|
||||
public readonly String m_mesh;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Tracing
|
||||
{
|
||||
public class AddressStack
|
||||
{
|
||||
// the first frame is the address of the last called method
|
||||
private readonly List<ulong> _stack;
|
||||
|
||||
public AddressStack( int capacity )
|
||||
{
|
||||
_stack = new List<ulong>( capacity );
|
||||
}
|
||||
|
||||
// No need to override GetHashCode because we don't want to use it as a key in a dictionary
|
||||
public override bool Equals( object obj )
|
||||
{
|
||||
if( obj == null )
|
||||
return false;
|
||||
|
||||
var stack = obj as AddressStack;
|
||||
if( stack == null )
|
||||
return false;
|
||||
|
||||
var frameCount = _stack.Count;
|
||||
if( frameCount != stack._stack.Count )
|
||||
return false;
|
||||
|
||||
for( int i = 0; i < frameCount; i++ )
|
||||
{
|
||||
if( _stack[i] != stack._stack[i] )
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override int GetHashCode() => _stack.GetHashCode();
|
||||
|
||||
public IReadOnlyList<ulong> Stack => _stack;
|
||||
|
||||
public void AddFrame( ulong address )
|
||||
{
|
||||
_stack.Add( address );
|
||||
}
|
||||
}
|
||||
}
|
||||
314
prof/Memory.cs
314
prof/Memory.cs
@ -1,314 +0,0 @@
|
||||
using Microsoft.Diagnostics.Tracing;
|
||||
using Microsoft.Diagnostics.Tracing.Parsers;
|
||||
using Microsoft.Diagnostics.Tracing.Parsers.Clr;
|
||||
using Microsoft.Diagnostics.Tracing.Parsers.Kernel;
|
||||
using Microsoft.Diagnostics.Tracing.Session;
|
||||
using ProfilerHelpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tracing
|
||||
{
|
||||
public class Memory
|
||||
{
|
||||
private readonly TraceEventSession _session;
|
||||
private readonly PerProcessProfilingState _processes;
|
||||
|
||||
// because we are not interested in self monitoring
|
||||
private readonly int _currentPid;
|
||||
|
||||
private int _started = 0;
|
||||
|
||||
public Memory( TraceEventSession session, PerProcessProfilingState processes )
|
||||
{
|
||||
_session = session;
|
||||
_processes = processes;
|
||||
_currentPid = Process.GetCurrentProcess().Id;
|
||||
}
|
||||
|
||||
public async Task StartAsync( bool allAllocations )
|
||||
{
|
||||
if( Interlocked.CompareExchange( ref _started, 1, 0 ) == 1 )
|
||||
{
|
||||
throw new InvalidOperationException( "Impossible to start profiling more than once." );
|
||||
}
|
||||
|
||||
await Task.Factory.StartNew( () =>
|
||||
{
|
||||
using( _session )
|
||||
{
|
||||
log.info( $"SetupProviders" );
|
||||
SetupProviders( _session, allAllocations );
|
||||
|
||||
log.info( $"SetupListeners" );
|
||||
SetupListeners( _session.Source );
|
||||
|
||||
log.info( $"Source.Process()" );
|
||||
while( _session.Source.Process() )
|
||||
{
|
||||
Task.Delay( 1 );
|
||||
}
|
||||
|
||||
log.info( $"Done" );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
private void SetupProviders( TraceEventSession session, bool noSampling )
|
||||
{
|
||||
// Note: the kernel provider MUST be the first provider to be enabled
|
||||
// If the kernel provider is not enabled, the callstacks for CLR events are still received
|
||||
// but the symbols are not found (except for the application itself)
|
||||
// Maybe a TraceEvent implementation details triggered when a module (image) is loaded
|
||||
var success = true;
|
||||
|
||||
//*
|
||||
log.info( $"EnableKernelProvider" );
|
||||
success = log.var( session.EnableKernelProvider( KernelTraceEventParser.Keywords.None |
|
||||
// KernelTraceEventParser.Keywords.ImageLoad |
|
||||
// KernelTraceEventParser.Keywords.Process
|
||||
0,
|
||||
KernelTraceEventParser.Keywords.None
|
||||
) );
|
||||
log.info( $"EnableKernelProvider {success}" );
|
||||
//*/
|
||||
|
||||
// The CLR source code indicates that the provider must be set before the monitored application starts
|
||||
// Note: no real difference between High and Low
|
||||
ClrTraceEventParser.Keywords eventsKeyword = noSampling
|
||||
? ClrTraceEventParser.Keywords.GCSampledObjectAllocationLow | ClrTraceEventParser.Keywords.GCSampledObjectAllocationHigh
|
||||
: ClrTraceEventParser.Keywords.GCSampledObjectAllocationLow
|
||||
;
|
||||
|
||||
log.info( $"EnableProvider" );
|
||||
success = log.var( session.EnableProvider(
|
||||
ClrTraceEventParser.ProviderGuid,
|
||||
TraceEventLevel.Verbose, // this is needed in order to receive GCSampledObjectAllocation event
|
||||
(ulong)(
|
||||
|
||||
eventsKeyword |
|
||||
|
||||
// required to receive the BulkType events that allows
|
||||
// mapping between the type ID received in the allocation events
|
||||
ClrTraceEventParser.Keywords.GCHeapAndTypeNames |
|
||||
ClrTraceEventParser.Keywords.Type |
|
||||
|
||||
// events related to JITed methods
|
||||
ClrTraceEventParser.Keywords.Jit | // Turning on JIT events is necessary to resolve JIT compiled code
|
||||
ClrTraceEventParser.Keywords.JittedMethodILToNativeMap | // This is needed if you want line number information in the stacks
|
||||
ClrTraceEventParser.Keywords.Loader | // You must include loader events as well to resolve JIT compiled code.
|
||||
|
||||
// this is mandatory to get the callstacks in each CLR event payload.
|
||||
//ClrTraceEventParser.Keywords.Stack |
|
||||
|
||||
0
|
||||
)
|
||||
) );
|
||||
log.info( $"EnableProvider {success}" );
|
||||
|
||||
|
||||
// Note: ClrRundown is not needed because only new processes will be monitored
|
||||
}
|
||||
|
||||
private void SetupListeners( ETWTraceEventSource source )
|
||||
{
|
||||
// register for high and low keyword
|
||||
// if both are set, each allocation will trigger an event (beware performance issues...)
|
||||
source.Clr.GCSampledObjectAllocation += OnSampleObjectAllocation;
|
||||
|
||||
// required to receive the mapping between type ID (received in GCSampledObjectAllocation)
|
||||
// and their name (received in TypeBulkType)
|
||||
source.Clr.TypeBulkType += OnTypeBulkType;
|
||||
|
||||
// messages to get callstacks
|
||||
// the correlation seems to be as "simple" as taking the last event on the same thread
|
||||
source.Clr.ClrStackWalk += OnClrStackWalk;
|
||||
|
||||
// needed to get JITed method details
|
||||
source.Clr.MethodLoadVerbose += OnMethodDetails;
|
||||
source.Clr.MethodDCStartVerboseV2 += OnMethodDetails;
|
||||
|
||||
source.Clr.ContentionLockCreated += OnLockCreated;
|
||||
source.Clr.ContentionStart += OnLockStart;
|
||||
source.Clr.ContentionStop += OnLockStop;
|
||||
|
||||
// get notified when a module is load to map the corresponding symbols
|
||||
source.Kernel.ImageLoad += OnImageLoad;
|
||||
}
|
||||
|
||||
private void OnLockCreated( ContentionLockCreatedTraceData data )
|
||||
{
|
||||
}
|
||||
|
||||
private void OnLockStart( ContentionStartTraceData data )
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private void OnLockStop( ContentionStopTraceData data )
|
||||
{
|
||||
}
|
||||
|
||||
private void OnImageLoad( ImageLoadTraceData data )
|
||||
{
|
||||
if( FilterOutEvent( data ) )
|
||||
return;
|
||||
|
||||
GetProcessMethods( data.ProcessID ).AddModule( data.FileName, data.ImageBase, data.ImageSize );
|
||||
|
||||
log.info( $"{data.ProcessID}.{data.ThreadID} --> {data.FileName}" );
|
||||
}
|
||||
|
||||
private void OnMethodDetails( MethodLoadUnloadVerboseTraceData data )
|
||||
{
|
||||
if( FilterOutEvent( data ) )
|
||||
return;
|
||||
|
||||
// care only about jitted methods
|
||||
if( !data.IsJitted )
|
||||
return;
|
||||
|
||||
var method = GetProcessMethods( data.ProcessID )
|
||||
.Add( data.MethodStartAddress, data.MethodSize, data.MethodNamespace, data.MethodName, data.MethodSignature );
|
||||
|
||||
log.info( $"0x{data.MethodStartAddress.ToString( "x12" )} - {data.MethodSize,6} | {data.MethodName}" );
|
||||
}
|
||||
|
||||
private MethodStore GetProcessMethods( int pid )
|
||||
{
|
||||
if( !_processes.Methods.TryGetValue( pid, out var methods ) )
|
||||
{
|
||||
methods = new MethodStore( pid );
|
||||
_processes.Methods[pid] = methods;
|
||||
}
|
||||
return methods;
|
||||
}
|
||||
|
||||
|
||||
private void OnSampleObjectAllocation( GCSampledObjectAllocationTraceData data )
|
||||
{
|
||||
if( FilterOutEvent( data ) )
|
||||
return;
|
||||
|
||||
var typeName = GetProcessTypeName( data.ProcessID, data.TypeID );
|
||||
if( data.TotalSizeForTypeSample >= 85000 )
|
||||
{
|
||||
var message = $"{data.ProcessID}.{data.ThreadID} - {data.TimeStampRelativeMSec,12} | Alloc {GetProcessTypeName( data.ProcessID, data.TypeID )} ({data.TotalSizeForTypeSample})";
|
||||
log.info( message );
|
||||
}
|
||||
GetProcessAllocations( data.ProcessID )
|
||||
.AddAllocation(
|
||||
data.ThreadID,
|
||||
(ulong)data.TotalSizeForTypeSample,
|
||||
(ulong)data.ObjectCountForTypeSample,
|
||||
typeName
|
||||
);
|
||||
}
|
||||
|
||||
private ProcessAllocations GetProcessAllocations( int pid )
|
||||
{
|
||||
if( !_processes.Allocations.TryGetValue( pid, out var allocations ) )
|
||||
{
|
||||
allocations = new ProcessAllocations( pid );
|
||||
_processes.Allocations[pid] = allocations;
|
||||
}
|
||||
return allocations;
|
||||
}
|
||||
|
||||
private void OnClrStackWalk( ClrStackWalkTraceData data )
|
||||
{
|
||||
if( FilterOutEvent( data ) )
|
||||
return;
|
||||
|
||||
//var message = $"{data.ProcessID}.{data.ThreadID} - {data.TimeStampRelativeMSec,12} | {data.FrameCount} frames";
|
||||
//log.info(message);
|
||||
|
||||
var callstack = BuildCallStack( data );
|
||||
GetProcessAllocations( data.ProcessID ).AddStack( data.ThreadID, callstack );
|
||||
//DumpStack(data);
|
||||
}
|
||||
|
||||
private AddressStack BuildCallStack( ClrStackWalkTraceData data )
|
||||
{
|
||||
var length = data.FrameCount;
|
||||
AddressStack stack = new AddressStack( length );
|
||||
|
||||
// frame 0 is the last frame of the stack (i.e. last called method)
|
||||
for( int i = 0; i < length; i++ )
|
||||
{
|
||||
stack.AddFrame( data.InstructionPointer( i ) );
|
||||
}
|
||||
|
||||
return stack;
|
||||
}
|
||||
|
||||
private void DumpStack( ClrStackWalkTraceData data )
|
||||
{
|
||||
var methods = GetProcessMethods( data.ProcessID );
|
||||
for( int i = 0; i < data.FrameCount; i++ )
|
||||
{
|
||||
var address = data.InstructionPointer( i );
|
||||
log.info( methods.GetFullName( address ) );
|
||||
}
|
||||
log.info( $"" );
|
||||
}
|
||||
|
||||
private void OnTypeBulkType( GCBulkTypeTraceData data )
|
||||
{
|
||||
if( FilterOutEvent( data ) )
|
||||
return;
|
||||
|
||||
ProcessTypeMapping mapping = GetProcessTypesMapping( data.ProcessID );
|
||||
for( int currentType = 0; currentType < data.Count; currentType++ )
|
||||
{
|
||||
GCBulkTypeValues value = data.Values( currentType );
|
||||
mapping[value.TypeID] = value.TypeName;
|
||||
}
|
||||
}
|
||||
|
||||
private ProcessTypeMapping GetProcessTypesMapping( int pid )
|
||||
{
|
||||
ProcessTypeMapping mapping;
|
||||
if( !_processes.Types.TryGetValue( pid, out mapping ) )
|
||||
{
|
||||
AssociateProcess( pid );
|
||||
|
||||
mapping = new ProcessTypeMapping( pid );
|
||||
_processes.Types[pid] = mapping;
|
||||
}
|
||||
return mapping;
|
||||
}
|
||||
|
||||
private void AssociateProcess( int pid )
|
||||
{
|
||||
try
|
||||
{
|
||||
_processes.Names[pid] = Process.GetProcessById( pid ).ProcessName;
|
||||
}
|
||||
catch( Exception )
|
||||
{
|
||||
log.info( $"? {pid}" );
|
||||
// we might not have access to the process
|
||||
}
|
||||
}
|
||||
|
||||
private string GetProcessTypeName( int pid, ulong typeID )
|
||||
{
|
||||
if( !_processes.Types.TryGetValue( pid, out var mapping ) )
|
||||
{
|
||||
return typeID.ToString();
|
||||
}
|
||||
|
||||
var name = mapping[typeID];
|
||||
return string.IsNullOrEmpty( name ) ? typeID.ToString() : name;
|
||||
}
|
||||
|
||||
private bool FilterOutEvent( TraceEvent data )
|
||||
{
|
||||
return ( data.ProcessID == _currentPid );
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,393 +0,0 @@
|
||||
using Microsoft.Diagnostics.NETCore.Client;
|
||||
using Microsoft.Diagnostics.Tracing;
|
||||
using Microsoft.Diagnostics.Tracing.Etlx;
|
||||
using Microsoft.Diagnostics.Tracing.Parsers;
|
||||
using Microsoft.Diagnostics.Tracing.Parsers.Clr;
|
||||
using Microsoft.Diagnostics.Tracing.Parsers.Kernel;
|
||||
using Microsoft.Diagnostics.Tracing.Session;
|
||||
using ProfilerHelpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Tracing;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using TraceKeywords = Microsoft.Diagnostics.Tracing.Parsers.ClrTraceEventParser.Keywords;
|
||||
|
||||
namespace Tracing
|
||||
{
|
||||
public class MemoryPipe
|
||||
{
|
||||
private readonly DiagnosticsClient _client;
|
||||
private readonly PerProcessProfilingState _processes;
|
||||
|
||||
// because we are not interested in self monitoring
|
||||
private readonly int _currentPid;
|
||||
|
||||
private int _started = 0;
|
||||
|
||||
Thread _thread;
|
||||
|
||||
public MemoryPipe()
|
||||
{
|
||||
|
||||
_currentPid = Process.GetCurrentProcess().Id;
|
||||
|
||||
_client = new DiagnosticsClient( _currentPid );
|
||||
|
||||
_processes = new();
|
||||
|
||||
|
||||
var threadStart = new ThreadStart( () => Start( true ) );
|
||||
_thread = new Thread( threadStart );
|
||||
_thread.Start();
|
||||
|
||||
|
||||
}
|
||||
|
||||
//public async Task StartAsync( bool allAllocations )
|
||||
public void Start( bool allAllocations )
|
||||
{
|
||||
if( Interlocked.CompareExchange( ref _started, 1, 0 ) == 1 )
|
||||
{
|
||||
throw new InvalidOperationException( "Impossible to start profiling more than once." );
|
||||
}
|
||||
|
||||
log.info( $"Starting MemoryPipe on thread {Thread.CurrentThread.ManagedThreadId}" );
|
||||
|
||||
var providers = new List<EventPipeProvider>()
|
||||
{
|
||||
new EventPipeProvider("Microsoft-Windows-DotNETRuntime",
|
||||
EventLevel.Verbose, (long)(
|
||||
TraceKeywords.GC |
|
||||
TraceKeywords.Contention |
|
||||
TraceKeywords.Debugger |
|
||||
TraceKeywords.Exception |
|
||||
TraceKeywords.GCAllObjectAllocation |
|
||||
TraceKeywords.GCSampledObjectAllocationHigh |
|
||||
TraceKeywords.GCSampledObjectAllocationLow |
|
||||
TraceKeywords.Security |
|
||||
TraceKeywords.Threading |
|
||||
TraceKeywords.Type |
|
||||
TraceKeywords.TypeDiagnostic |
|
||||
TraceKeywords.WaitHandle |
|
||||
TraceKeywords.All
|
||||
) )
|
||||
};
|
||||
|
||||
|
||||
//await Task.Factory.StartNew( () => {
|
||||
using EventPipeSession session = _client.StartEventPipeSession( providers, false );
|
||||
|
||||
//log.info( $"SetupProviders" );
|
||||
//SetupProviders( session, allAllocations );
|
||||
var source = new EventPipeEventSource( session.EventStream );
|
||||
|
||||
|
||||
log.info( $"SetupListeners" );
|
||||
SetupListeners( source );
|
||||
|
||||
log.info( $"Run Process" );
|
||||
source.Process();
|
||||
|
||||
log.info( $"Done" );
|
||||
//} );
|
||||
}
|
||||
|
||||
private void SetupProviders( TraceEventSession session, bool noSampling )
|
||||
{
|
||||
// Note: the kernel provider MUST be the first provider to be enabled
|
||||
// If the kernel provider is not enabled, the callstacks for CLR events are still received
|
||||
// but the symbols are not found (except for the application itself)
|
||||
// Maybe a TraceEvent implementation details triggered when a module (image) is loaded
|
||||
var success = true;
|
||||
|
||||
//*
|
||||
log.info( $"EnableKernelProvider" );
|
||||
success = log.var( session.EnableKernelProvider( KernelTraceEventParser.Keywords.None |
|
||||
// KernelTraceEventParser.Keywords.ImageLoad |
|
||||
// KernelTraceEventParser.Keywords.Process
|
||||
0,
|
||||
KernelTraceEventParser.Keywords.None
|
||||
) );
|
||||
log.info( $"EnableKernelProvider {success}" );
|
||||
//*/
|
||||
|
||||
// The CLR source code indicates that the provider must be set before the monitored application starts
|
||||
// Note: no real difference between High and Low
|
||||
ClrTraceEventParser.Keywords eventsKeyword = noSampling
|
||||
? ClrTraceEventParser.Keywords.GCSampledObjectAllocationLow | ClrTraceEventParser.Keywords.GCSampledObjectAllocationHigh
|
||||
: ClrTraceEventParser.Keywords.GCSampledObjectAllocationLow
|
||||
;
|
||||
|
||||
log.info( $"EnableProvider" );
|
||||
success = log.var( session.EnableProvider(
|
||||
ClrTraceEventParser.ProviderGuid,
|
||||
TraceEventLevel.Verbose, // this is needed in order to receive GCSampledObjectAllocation event
|
||||
(ulong)(
|
||||
|
||||
eventsKeyword |
|
||||
|
||||
// required to receive the BulkType events that allows
|
||||
// mapping between the type ID received in the allocation events
|
||||
ClrTraceEventParser.Keywords.GCHeapAndTypeNames |
|
||||
ClrTraceEventParser.Keywords.Type |
|
||||
|
||||
// events related to JITed methods
|
||||
ClrTraceEventParser.Keywords.Jit | // Turning on JIT events is necessary to resolve JIT compiled code
|
||||
ClrTraceEventParser.Keywords.JittedMethodILToNativeMap | // This is needed if you want line number information in the stacks
|
||||
ClrTraceEventParser.Keywords.Loader | // You must include loader events as well to resolve JIT compiled code.
|
||||
|
||||
// this is mandatory to get the callstacks in each CLR event payload.
|
||||
//ClrTraceEventParser.Keywords.Stack |
|
||||
|
||||
0
|
||||
)
|
||||
) );
|
||||
log.info( $"EnableProvider {success}" );
|
||||
|
||||
|
||||
// Note: ClrRundown is not needed because only new processes will be monitored
|
||||
}
|
||||
|
||||
private void SetupListeners( EventPipeEventSource source )
|
||||
{
|
||||
// register for high and low keyword
|
||||
// if both are set, each allocation will trigger an event (beware performance issues...)
|
||||
//source.Clr.GCSampledObjectAllocation += OnSampleObjectAllocation;
|
||||
source.Clr.GCAllocationTick += OnAllocTick;
|
||||
|
||||
// required to receive the mapping between type ID (received in GCSampledObjectAllocation)
|
||||
// and their name (received in TypeBulkType)
|
||||
source.Clr.TypeBulkType += OnTypeBulkType;
|
||||
|
||||
// messages to get callstacks
|
||||
// the correlation seems to be as "simple" as taking the last event on the same thread
|
||||
source.Clr.ClrStackWalk += OnClrStackWalk;
|
||||
|
||||
// needed to get JITed method details
|
||||
source.Clr.MethodLoadVerbose += OnMethodDetails;
|
||||
source.Clr.MethodDCStartVerboseV2 += OnMethodDetails;
|
||||
|
||||
source.Clr.ContentionLockCreated += OnLockCreated;
|
||||
source.Clr.ContentionStart += OnLockStart;
|
||||
source.Clr.ContentionStop += OnLockStop;
|
||||
|
||||
// get notified when a module is load to map the corresponding symbols
|
||||
source.Kernel.ImageLoad += OnImageLoad;
|
||||
}
|
||||
|
||||
private void OnAllocTick( GCAllocationTickTraceData data )
|
||||
{
|
||||
if( FilterOutEvent( data ) )
|
||||
return;
|
||||
|
||||
//log.info( $"*** RAW: {data}" );
|
||||
|
||||
//var callStack = data.CallStack();
|
||||
//var caller = callStack.Caller;
|
||||
|
||||
//log.info( $"Call stack {callStack}" );
|
||||
|
||||
var thisThreadId = Thread.CurrentThread.ManagedThreadId;
|
||||
|
||||
var typeName = GetProcessTypeName( data.ProcessID, data.TypeID );
|
||||
//if( data.TotalSizeForTypeSample >= 85000 )
|
||||
{
|
||||
var message = $"{data.ThreadID}/{thisThreadId} Alloc {data.ObjectSize,8} at 0x{data.Address:0000000000000000} in {data.TypeName} (or {GetProcessTypeName( data.ProcessID, data.TypeID )}) ";
|
||||
log.info( message );
|
||||
}
|
||||
GetProcessAllocations( data.ProcessID )
|
||||
.AddAllocation(
|
||||
data.ThreadID,
|
||||
(ulong)data.ObjectSize,
|
||||
(ulong)1,
|
||||
typeName
|
||||
);
|
||||
}
|
||||
|
||||
private void OnLockCreated( ContentionLockCreatedTraceData data )
|
||||
{
|
||||
log.info( $"{data}" );
|
||||
}
|
||||
|
||||
private void OnLockStart( ContentionStartTraceData data )
|
||||
{
|
||||
log.info( $"{data}" );
|
||||
}
|
||||
|
||||
private void OnLockStop( ContentionStopTraceData data )
|
||||
{
|
||||
log.info( $"{data}" );
|
||||
}
|
||||
|
||||
private void OnImageLoad( ImageLoadTraceData data )
|
||||
{
|
||||
//if( FilterOutEvent( data ) )
|
||||
// return;
|
||||
|
||||
log.info( $"{data}" );
|
||||
|
||||
//GetProcessMethods( data.ProcessID ).AddModule( data.FileName, data.ImageBase, data.ImageSize );
|
||||
|
||||
log.info( $"{data.ProcessID}.{data.ThreadID} --> {data.FileName}" );
|
||||
}
|
||||
|
||||
private void OnMethodDetails( MethodLoadUnloadVerboseTraceData data )
|
||||
{
|
||||
//if( FilterOutEvent( data ) )
|
||||
// return;
|
||||
|
||||
//log.info( $"{data}" );
|
||||
|
||||
// care only about jitted methods
|
||||
if( !data.IsJitted )
|
||||
return;
|
||||
|
||||
var method = GetProcessMethods( data.ProcessID )
|
||||
.Add( data.MethodStartAddress, data.MethodSize, data.MethodNamespace, data.MethodName, data.MethodSignature );
|
||||
|
||||
//log.info( $"0x{data.MethodStartAddress.ToString( "x12" )} - {data.MethodSize,6} | {data.MethodName}" );
|
||||
}
|
||||
|
||||
private MethodStore GetProcessMethods( int pid )
|
||||
{
|
||||
if( !_processes.Methods.TryGetValue( pid, out var methods ) )
|
||||
{
|
||||
methods = new MethodStore( pid );
|
||||
_processes.Methods[pid] = methods;
|
||||
}
|
||||
return methods;
|
||||
}
|
||||
|
||||
|
||||
private void OnSampleObjectAllocation( GCSampledObjectAllocationTraceData data )
|
||||
{
|
||||
if( FilterOutEvent( data ) )
|
||||
return;
|
||||
|
||||
//log.info( $"{data}" );
|
||||
|
||||
var typeName = GetProcessTypeName( data.ProcessID, data.TypeID );
|
||||
//if( data.TotalSizeForTypeSample >= 85000 )
|
||||
{
|
||||
var message = $"{data.ProcessID}.{data.ThreadID} - {data.TimeStampRelativeMSec,12} | Alloc {GetProcessTypeName( data.ProcessID, data.TypeID )} ({data.TotalSizeForTypeSample})";
|
||||
log.info( message );
|
||||
}
|
||||
GetProcessAllocations( data.ProcessID )
|
||||
.AddAllocation(
|
||||
data.ThreadID,
|
||||
(ulong)data.TotalSizeForTypeSample,
|
||||
(ulong)data.ObjectCountForTypeSample,
|
||||
typeName
|
||||
);
|
||||
}
|
||||
|
||||
private ProcessAllocations GetProcessAllocations( int pid )
|
||||
{
|
||||
if( !_processes.Allocations.TryGetValue( pid, out var allocations ) )
|
||||
{
|
||||
allocations = new ProcessAllocations( pid );
|
||||
_processes.Allocations[pid] = allocations;
|
||||
}
|
||||
return allocations;
|
||||
}
|
||||
|
||||
private void OnClrStackWalk( ClrStackWalkTraceData data )
|
||||
{
|
||||
var message = $"{data.ProcessID}.{data.ThreadID} - {data.TimeStampRelativeMSec,12} | {data.FrameCount} frames";
|
||||
log.info( message );
|
||||
|
||||
var callstack = BuildCallStack( data );
|
||||
GetProcessAllocations( data.ProcessID ).AddStack( data.ThreadID, callstack );
|
||||
//DumpStack(data);
|
||||
}
|
||||
|
||||
private AddressStack BuildCallStack( ClrStackWalkTraceData data )
|
||||
{
|
||||
//log.info( $"{data}" );
|
||||
|
||||
var length = data.FrameCount;
|
||||
AddressStack stack = new AddressStack( length );
|
||||
|
||||
// frame 0 is the last frame of the stack (i.e. last called method)
|
||||
for( int i = 0; i < length; i++ )
|
||||
{
|
||||
stack.AddFrame( data.InstructionPointer( i ) );
|
||||
}
|
||||
|
||||
return stack;
|
||||
}
|
||||
|
||||
private void DumpStack( ClrStackWalkTraceData data )
|
||||
{
|
||||
if( FilterOutEvent( data ) )
|
||||
return;
|
||||
|
||||
log.info( $"{data}" );
|
||||
|
||||
var methods = GetProcessMethods( data.ProcessID );
|
||||
for( int i = 0; i < data.FrameCount; i++ )
|
||||
{
|
||||
var address = data.InstructionPointer( i );
|
||||
log.info( methods.GetFullName( address ) );
|
||||
}
|
||||
log.info( $"" );
|
||||
}
|
||||
|
||||
private void OnTypeBulkType( GCBulkTypeTraceData data )
|
||||
{
|
||||
|
||||
ProcessTypeMapping mapping = GetProcessTypesMapping( data.ProcessID );
|
||||
for( int currentType = 0; currentType < data.Count; currentType++ )
|
||||
{
|
||||
GCBulkTypeValues value = data.Values( currentType );
|
||||
mapping[value.TypeID] = value.TypeName;
|
||||
//log.info( $"{value}" );
|
||||
}
|
||||
}
|
||||
|
||||
private ProcessTypeMapping GetProcessTypesMapping( int pid )
|
||||
{
|
||||
ProcessTypeMapping mapping;
|
||||
if( !_processes.Types.TryGetValue( pid, out mapping ) )
|
||||
{
|
||||
AssociateProcess( pid );
|
||||
|
||||
mapping = new ProcessTypeMapping( pid );
|
||||
_processes.Types[pid] = mapping;
|
||||
}
|
||||
return mapping;
|
||||
}
|
||||
|
||||
private void AssociateProcess( int pid )
|
||||
{
|
||||
try
|
||||
{
|
||||
_processes.Names[pid] = Process.GetProcessById( pid ).ProcessName;
|
||||
}
|
||||
catch( Exception )
|
||||
{
|
||||
log.info( $"? {pid}" );
|
||||
// we might not have access to the process
|
||||
}
|
||||
}
|
||||
|
||||
private string GetProcessTypeName( int pid, ulong typeID )
|
||||
{
|
||||
if( !_processes.Types.TryGetValue( pid, out var mapping ) )
|
||||
{
|
||||
return typeID.ToString();
|
||||
}
|
||||
|
||||
var name = mapping[typeID];
|
||||
return string.IsNullOrEmpty( name ) ? typeID.ToString() : name;
|
||||
}
|
||||
|
||||
private bool FilterOutEvent( TraceEvent data )
|
||||
{
|
||||
return data.ThreadID == Thread.CurrentThread.ManagedThreadId;
|
||||
//return false; // ( data.ProcessID == _currentPid );
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,68 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace ProfilerHelpers
|
||||
{
|
||||
public class MethodInfo
|
||||
{
|
||||
private readonly ulong _startAddress;
|
||||
private readonly int _size;
|
||||
private readonly string _fullName;
|
||||
|
||||
internal MethodInfo( ulong startAddress, int size, string namespaceAndTypeName, string name, string signature )
|
||||
{
|
||||
_startAddress = startAddress;
|
||||
_size = size;
|
||||
_fullName = ComputeFullName( startAddress, namespaceAndTypeName, name, signature );
|
||||
}
|
||||
|
||||
private string ComputeFullName( ulong startAddress, string namespaceAndTypeName, string name, string signature )
|
||||
{
|
||||
var fullName = signature;
|
||||
|
||||
// constructor case: name = .ctor | namespaceAndTypeName = A.B.typeName | signature = ... (parameters)
|
||||
// --> A.B.typeName(parameters)
|
||||
if( name == ".ctor" )
|
||||
{
|
||||
return $"{namespaceAndTypeName}{ExtractParameters( signature )}";
|
||||
}
|
||||
|
||||
// general case: name = Foo | namespaceAndTypeName = A.B.typeName | signature = ... (parameters)
|
||||
// --> A.B.Foo(parameters)
|
||||
fullName = $"{namespaceAndTypeName}.{name}{ExtractParameters( signature )}";
|
||||
return fullName;
|
||||
}
|
||||
|
||||
private string ExtractTypeName( string namespaceAndTypeName )
|
||||
{
|
||||
var pos = namespaceAndTypeName.LastIndexOf( ".", StringComparison.Ordinal );
|
||||
if( pos == -1 )
|
||||
{
|
||||
return namespaceAndTypeName;
|
||||
}
|
||||
|
||||
// skip the .
|
||||
pos++;
|
||||
|
||||
return namespaceAndTypeName.Substring( pos );
|
||||
}
|
||||
|
||||
private string ExtractParameters( string signature )
|
||||
{
|
||||
var pos = signature.IndexOf( " (" );
|
||||
if( pos == -1 )
|
||||
{
|
||||
return "(???)";
|
||||
}
|
||||
|
||||
// skip double space
|
||||
pos += 2;
|
||||
|
||||
var parameters = signature.Substring( pos );
|
||||
return parameters;
|
||||
}
|
||||
|
||||
public ulong StartAddress => _startAddress;
|
||||
public int Size => _size;
|
||||
public string FullName => _fullName;
|
||||
}
|
||||
}
|
||||
@ -1,209 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace ProfilerHelpers
|
||||
{
|
||||
public class MethodStore : IDisposable
|
||||
{
|
||||
// JITed methods information (start address + size + signature)
|
||||
private readonly List<MethodInfo> _methods;
|
||||
|
||||
// addresses from callstacks already matching (address -> full name)
|
||||
private readonly Dictionary<ulong, string> _cache;
|
||||
|
||||
// for native methods, rely on dbghelp API
|
||||
// so the process handle is required
|
||||
private IntPtr _hProcess;
|
||||
private Process _process;
|
||||
private readonly int _pid;
|
||||
|
||||
public MethodStore( int pid, bool loadModules = false )
|
||||
{
|
||||
// it may be possible to open the process
|
||||
// in that case, _hProcess = IntPtr.Zero
|
||||
_pid = pid;
|
||||
|
||||
_methods = new List<MethodInfo>( 1024 );
|
||||
_cache = new Dictionary<ulong, string>();
|
||||
|
||||
_hProcess = BindToProcess( pid, loadModules );
|
||||
}
|
||||
|
||||
private IntPtr BindToProcess( int pid, bool loadModules )
|
||||
{
|
||||
try
|
||||
{
|
||||
_process = Process.GetProcessById( pid );
|
||||
|
||||
if( !SymInitialize( _process.Handle, loadModules ) )
|
||||
return IntPtr.Zero;
|
||||
|
||||
return _process.Handle;
|
||||
}
|
||||
catch( Exception x )
|
||||
{
|
||||
Console.WriteLine( $"Error while binding pid #{pid} to DbgHelp:" );
|
||||
Console.WriteLine( x.Message );
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
private bool SymInitialize( IntPtr hProcess, bool loadModules = false )
|
||||
{
|
||||
// read https://docs.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-symsetoptions for more details
|
||||
// maybe SYMOPT_NO_PROMPTS and SYMOPT_FAIL_CRITICAL_ERRORS could be used
|
||||
NativeDbgHelp.SymSetOptions(
|
||||
NativeDbgHelp.SYMOPT_DEFERRED_LOADS | // performance optimization
|
||||
NativeDbgHelp.SYMOPT_UNDNAME // C++ names are not mangled
|
||||
);
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/dbghelp/nf-dbghelp-syminitialize
|
||||
// search path for symbols:
|
||||
// - The current working directory of the application
|
||||
// - The _NT_SYMBOL_PATH environment variable
|
||||
// - The _NT_ALTERNATE_SYMBOL_PATH environment variable
|
||||
//
|
||||
// passing false as last parameter means that we will need to call SymLoadModule64
|
||||
// each time a module is loaded in the process
|
||||
return NativeDbgHelp.SymInitialize( hProcess, null, loadModules );
|
||||
}
|
||||
|
||||
public MethodInfo Add( ulong address, int size, string namespaceAndTypeName, string name, string signature )
|
||||
{
|
||||
var method = new MethodInfo( address, size, namespaceAndTypeName, name, signature );
|
||||
_methods.Add( method );
|
||||
return method;
|
||||
}
|
||||
|
||||
public string GetFullName( ulong address )
|
||||
{
|
||||
if( _cache.TryGetValue( address, out var fullName ) )
|
||||
return fullName;
|
||||
|
||||
// look for managed methods
|
||||
for( int i = 0; i < _methods.Count; i++ )
|
||||
{
|
||||
var method = _methods[i];
|
||||
|
||||
if( ( address >= method.StartAddress ) && ( address < method.StartAddress + (ulong)method.Size ) )
|
||||
{
|
||||
fullName = method.FullName;
|
||||
_cache[address] = fullName;
|
||||
return fullName;
|
||||
}
|
||||
}
|
||||
|
||||
// look for native methods
|
||||
fullName = GetNativeMethodName( address );
|
||||
_cache[address] = fullName;
|
||||
|
||||
return fullName;
|
||||
}
|
||||
|
||||
private string GetNativeMethodName( ulong address )
|
||||
{
|
||||
var symbol = new NativeDbgHelp.SYMBOL_INFO();
|
||||
symbol.MaxNameLen = 1024;
|
||||
symbol.SizeOfStruct = (uint)Marshal.SizeOf( symbol ) - 1024; // char buffer is not counted
|
||||
// the ANSI version of SymFromAddr is called so each character is 1 byte long
|
||||
|
||||
if( NativeDbgHelp.SymFromAddr( _hProcess, address, out var displacement, ref symbol ) )
|
||||
{
|
||||
var buffer = new StringBuilder( symbol.Name.Length );
|
||||
|
||||
// remove weird "$##" at the end of some symbols
|
||||
var pos = symbol.Name.LastIndexOf( "$##" );
|
||||
if( pos == -1 )
|
||||
buffer.Append( symbol.Name );
|
||||
else
|
||||
buffer.Append( symbol.Name, 0, pos );
|
||||
|
||||
// add offset if any
|
||||
if( displacement != 0 )
|
||||
buffer.Append( $"+0x{displacement}" );
|
||||
|
||||
return buffer.ToString();
|
||||
}
|
||||
|
||||
// default value is just the address in HEX
|
||||
#if DEBUG
|
||||
return ( $"0x{address:x} (SymFromAddr failed with 0x{Marshal.GetLastWin32Error():x})" );
|
||||
#else
|
||||
return $"0x{address:x}";
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
const int ERROR_SUCCESS = 0;
|
||||
public void AddModule( string filename, ulong baseOfDll, int sizeOfDll )
|
||||
{
|
||||
var baseAddress = NativeDbgHelp.SymLoadModule64( _hProcess, IntPtr.Zero, filename, null, baseOfDll, (uint)sizeOfDll );
|
||||
if( baseAddress == 0 )
|
||||
{
|
||||
// should work if the same module is added more than once
|
||||
if( Marshal.GetLastWin32Error() == ERROR_SUCCESS )
|
||||
return;
|
||||
|
||||
Console.WriteLine( $"SymLoadModule64 failed for {filename}" );
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if( _hProcess == IntPtr.Zero )
|
||||
return;
|
||||
_hProcess = IntPtr.Zero;
|
||||
|
||||
_process.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
internal static class NativeDbgHelp
|
||||
{
|
||||
// from C:\Program Files (x86)\Windows Kits\10\Debuggers\inc\dbghelp.h
|
||||
public const uint SYMOPT_UNDNAME = 0x00000002;
|
||||
public const uint SYMOPT_DEFERRED_LOADS = 0x00000004;
|
||||
|
||||
[StructLayout( LayoutKind.Sequential )]
|
||||
public struct SYMBOL_INFO
|
||||
{
|
||||
public uint SizeOfStruct;
|
||||
public uint TypeIndex; // Type Index of symbol
|
||||
private ulong Reserved1;
|
||||
private ulong Reserved2;
|
||||
public uint Index;
|
||||
public uint Size;
|
||||
public ulong ModBase; // Base Address of module containing this symbol
|
||||
public uint Flags;
|
||||
public ulong Value; // Value of symbol, ValuePresent should be 1
|
||||
public ulong Address; // Address of symbol including base address of module
|
||||
public uint Register; // register holding value or pointer to value
|
||||
public uint Scope; // scope of the symbol
|
||||
public uint Tag; // pdb classification
|
||||
public uint NameLen; // Actual length of name
|
||||
public uint MaxNameLen;
|
||||
[MarshalAs( UnmanagedType.ByValTStr, SizeConst = 1024 )]
|
||||
public string Name;
|
||||
}
|
||||
|
||||
[DllImport( "dbghelp.dll", SetLastError = true )]
|
||||
public static extern bool SymInitialize( IntPtr hProcess, string userSearchPath, bool invadeProcess );
|
||||
|
||||
[DllImport( "dbghelp.dll", SetLastError = true )]
|
||||
public static extern uint SymSetOptions( uint symOptions );
|
||||
|
||||
[DllImport( "dbghelp.dll", SetLastError = true, CharSet = CharSet.Ansi )]
|
||||
public static extern ulong SymLoadModule64( IntPtr hProcess, IntPtr hFile, string imageName, string moduleName, ulong baseOfDll, uint sizeOfDll );
|
||||
|
||||
// use ANSI version to ensure the right size of the structure
|
||||
// read https://docs.microsoft.com/en-us/windows/win32/api/dbghelp/ns-dbghelp-symbol_info
|
||||
[DllImport( "dbghelp.dll", SetLastError = true, CharSet = CharSet.Ansi )]
|
||||
public static extern bool SymFromAddr( IntPtr hProcess, ulong address, out ulong displacement, ref SYMBOL_INFO symbol );
|
||||
|
||||
[DllImport( "dbghelp.dll", SetLastError = true )]
|
||||
public static extern bool SymCleanup( IntPtr hProcess );
|
||||
}
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
using ProfilerHelpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Tracing
|
||||
{
|
||||
public class PerProcessProfilingState : IDisposable
|
||||
{
|
||||
private bool _disposed;
|
||||
|
||||
private readonly Dictionary<int, string> _processNames = new Dictionary<int, string>();
|
||||
private readonly Dictionary<int, ProcessTypeMapping> _perProcessTypes = new Dictionary<int, ProcessTypeMapping>();
|
||||
private readonly Dictionary<int, ProcessAllocations> _perProcessAllocations = new Dictionary<int, ProcessAllocations>();
|
||||
private readonly Dictionary<int, MethodStore> _methods = new Dictionary<int, MethodStore>();
|
||||
|
||||
public Dictionary<int, string> Names => _processNames;
|
||||
public Dictionary<int, ProcessTypeMapping> Types => _perProcessTypes;
|
||||
public Dictionary<int, ProcessAllocations> Allocations => _perProcessAllocations;
|
||||
public Dictionary<int, MethodStore> Methods => _methods;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if( _disposed )
|
||||
return;
|
||||
_disposed = true;
|
||||
|
||||
foreach( var methodStore in _methods.Values )
|
||||
{
|
||||
methodStore.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,131 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Tracing
|
||||
{
|
||||
|
||||
public class ProcessAllocations
|
||||
{
|
||||
private readonly int _pid;
|
||||
private readonly Dictionary<string, AllocationInfo> _allocations;
|
||||
private readonly Dictionary<int, AllocationInfo> _perThreadLastAllocation;
|
||||
|
||||
public ProcessAllocations( int pid )
|
||||
{
|
||||
_pid = pid;
|
||||
_allocations = new Dictionary<string, AllocationInfo>();
|
||||
_perThreadLastAllocation = new Dictionary<int, AllocationInfo>();
|
||||
}
|
||||
|
||||
public int Pid => _pid;
|
||||
|
||||
public AllocationInfo GetAllocations( string typeName )
|
||||
{
|
||||
return ( _allocations.TryGetValue( typeName, out var info ) ) ? info : null;
|
||||
}
|
||||
|
||||
public IEnumerable<AllocationInfo> GetAllAllocations()
|
||||
{
|
||||
return _allocations.Values;
|
||||
}
|
||||
|
||||
public AllocationInfo AddAllocation( int threadID, ulong size, ulong count, string typeName )
|
||||
{
|
||||
if( !_allocations.TryGetValue( typeName, out var info ) )
|
||||
{
|
||||
info = new AllocationInfo( typeName );
|
||||
_allocations[typeName] = info;
|
||||
}
|
||||
|
||||
info.AddAllocation( size, count );
|
||||
|
||||
// the last allocation is still here without the corresponding stack
|
||||
if( _perThreadLastAllocation.TryGetValue( threadID, out var lastAlloc ) )
|
||||
{
|
||||
Console.WriteLine( "no stack for the last allocation" );
|
||||
}
|
||||
|
||||
// keep track of the allocation for the given thread
|
||||
// --> will be used when the corresponding call stack event will be received
|
||||
_perThreadLastAllocation[threadID] = info;
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
public void AddStack( int threadID, AddressStack stack )
|
||||
{
|
||||
if( _perThreadLastAllocation.TryGetValue( threadID, out var lastAlloc ) )
|
||||
{
|
||||
lastAlloc.AddStack( stack );
|
||||
_perThreadLastAllocation.Remove( threadID );
|
||||
return;
|
||||
}
|
||||
|
||||
//Console.WriteLine("no last allocation for the stack event");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class AllocationInfo
|
||||
{
|
||||
private readonly string _typeName;
|
||||
private ulong _size;
|
||||
private ulong _count;
|
||||
private List<StackInfo> _stacks;
|
||||
|
||||
internal AllocationInfo( string typeName )
|
||||
{
|
||||
_typeName = typeName;
|
||||
_stacks = new List<StackInfo>();
|
||||
}
|
||||
|
||||
public string TypeName => _typeName;
|
||||
public ulong Count => _count;
|
||||
public ulong Size => _size;
|
||||
public IReadOnlyList<StackInfo> Stacks => _stacks;
|
||||
|
||||
internal void AddAllocation( ulong size, ulong count )
|
||||
{
|
||||
_count += count;
|
||||
_size += size;
|
||||
}
|
||||
|
||||
internal void AddStack( AddressStack stack )
|
||||
{
|
||||
var info = GetInfo( stack );
|
||||
if( info == null )
|
||||
{
|
||||
info = new StackInfo( stack );
|
||||
_stacks.Add( info );
|
||||
}
|
||||
|
||||
info.Count++;
|
||||
}
|
||||
|
||||
private StackInfo GetInfo( AddressStack stack )
|
||||
{
|
||||
for( int i = 0; i < _stacks.Count; i++ )
|
||||
{
|
||||
var info = _stacks[i];
|
||||
if( stack.Equals( info.Stack ) )
|
||||
return info;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public class StackInfo
|
||||
{
|
||||
private readonly AddressStack _stack;
|
||||
public ulong Count;
|
||||
|
||||
internal StackInfo( AddressStack stack )
|
||||
{
|
||||
Count = 0;
|
||||
_stack = stack;
|
||||
}
|
||||
|
||||
public AddressStack Stack => _stack;
|
||||
}
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Tracing
|
||||
{
|
||||
// Contains the mapping between type ID received by SampleObjectAllocation(Low/High) events
|
||||
// and their name received by TypeBulkType events
|
||||
public class ProcessTypeMapping
|
||||
{
|
||||
private readonly Dictionary<ulong, string> _typesIdToName;
|
||||
|
||||
public ProcessTypeMapping( int processId )
|
||||
{
|
||||
ProcessId = processId;
|
||||
_typesIdToName = new Dictionary<ulong, string>();
|
||||
}
|
||||
|
||||
public int ProcessId { get; set; }
|
||||
|
||||
public string this[ulong id]
|
||||
{
|
||||
get
|
||||
{
|
||||
if( !_typesIdToName.ContainsKey( id ) )
|
||||
return null;
|
||||
|
||||
return _typesIdToName[id];
|
||||
}
|
||||
set
|
||||
{
|
||||
_typesIdToName[id] = value;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
162
prof/Program.cs
162
prof/Program.cs
@ -1,162 +0,0 @@
|
||||
using Microsoft.Diagnostics.Tracing.Session;
|
||||
using ProfilerHelpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tracing
|
||||
{
|
||||
static class Program
|
||||
{
|
||||
|
||||
|
||||
static public int CreateTracingSession( bool noSampling, bool sortBySize, int topTypesLimit )
|
||||
{
|
||||
ShowHeader();
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
TraceEventSession session = new TraceEventSession(
|
||||
"Tracing",
|
||||
TraceEventSessionOptions.Create
|
||||
);
|
||||
|
||||
log.info( $"Create PerProcessProfilingState" );
|
||||
|
||||
using( var processes = new PerProcessProfilingState() )
|
||||
{
|
||||
log.info( $"Create Memory profiler for session" );
|
||||
var profiler = new Memory( session, processes );
|
||||
|
||||
log.info( $"Start task" );
|
||||
var task = profiler.StartAsync( noSampling );
|
||||
|
||||
/*
|
||||
log.info( $"await the Continue" );
|
||||
await task.ContinueWith( ( t ) => {
|
||||
log.info( $"Task is done, Dispose" );
|
||||
session.Dispose();
|
||||
} );
|
||||
*/
|
||||
|
||||
//log.info("Press ENTER to stop memory profiling");
|
||||
//Console.ReadLine();
|
||||
|
||||
/*
|
||||
try
|
||||
{
|
||||
await task;
|
||||
ShowResults( processes, sortBySize, topTypesLimit );
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch( Exception x )
|
||||
{
|
||||
log.info( x.Message );
|
||||
ShowHelp();
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
catch( Exception x )
|
||||
{
|
||||
log.info( x.Message );
|
||||
ShowHelp();
|
||||
}
|
||||
|
||||
return -2;
|
||||
}
|
||||
|
||||
private static void ShowResults( PerProcessProfilingState processes, bool sortBySize, int topTypesLimit )
|
||||
{
|
||||
foreach( var pid in processes.Allocations.Keys )
|
||||
{
|
||||
// skip processes without symbol resolution
|
||||
if( !processes.Methods.ContainsKey( pid ) )
|
||||
continue;
|
||||
|
||||
// skip processes without allocations
|
||||
if( !processes.Allocations[pid].GetAllAllocations().Any() )
|
||||
continue;
|
||||
|
||||
ShowResults( GetProcessName( pid, processes.Names ), processes.Methods[pid], processes.Allocations[pid], sortBySize, topTypesLimit );
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetProcessName( int pid, Dictionary<int, string> names )
|
||||
{
|
||||
if( names.TryGetValue( pid, out var name ) )
|
||||
return name;
|
||||
|
||||
return pid.ToString();
|
||||
}
|
||||
|
||||
private static void ShowResults( string name, MethodStore methods, ProcessAllocations allocations, bool sortBySize, int topTypesLimit )
|
||||
{
|
||||
log.info( $"Memory allocations for {name}" );
|
||||
log.info( $"" );
|
||||
log.info( "---------------------------------------------------------" );
|
||||
log.info( " Count Size Type" );
|
||||
log.info( "---------------------------------------------------------" );
|
||||
IEnumerable<AllocationInfo> types = ( sortBySize )
|
||||
? allocations.GetAllAllocations().OrderByDescending( a => a.Size )
|
||||
: allocations.GetAllAllocations().OrderByDescending( a => a.Count )
|
||||
;
|
||||
if( topTypesLimit != -1 )
|
||||
types = types.Take( topTypesLimit );
|
||||
|
||||
foreach( var allocation in types )
|
||||
{
|
||||
log.info( $"{allocation.Count,9} {allocation.Size,11} {allocation.TypeName}" );
|
||||
|
||||
log.info( $"" );
|
||||
DumpStacks( allocation, methods );
|
||||
log.info( $"" );
|
||||
}
|
||||
log.info( $"" );
|
||||
log.info( $"" );
|
||||
}
|
||||
|
||||
private static void DumpStacks( AllocationInfo allocation, MethodStore methods )
|
||||
{
|
||||
var stacks = allocation.Stacks.OrderByDescending( s => s.Count ).Take( 10 );
|
||||
foreach( var stack in stacks )
|
||||
{
|
||||
log.info( $"{stack.Count,6} allocations" );
|
||||
log.info( "----------------------------------" );
|
||||
DumpStack( stack.Stack, methods );
|
||||
log.info( $"" );
|
||||
}
|
||||
}
|
||||
|
||||
private static void DumpStack( AddressStack stack, MethodStore methods )
|
||||
{
|
||||
var callstack = stack.Stack;
|
||||
for( int i = 0; i < Math.Min( 10, callstack.Count ); i++ )
|
||||
{
|
||||
log.info( $" {methods.GetFullName( callstack[i] )}" );
|
||||
}
|
||||
}
|
||||
|
||||
private static void ShowHeader()
|
||||
{
|
||||
log.info( "Tracing v1.0.0 - Sampled memory profiler for .NET applications" );
|
||||
log.info( "by Christophe Nasarre" );
|
||||
log.info( $"" );
|
||||
}
|
||||
private static void ShowHelp()
|
||||
{
|
||||
log.info( $"" );
|
||||
log.info( "Tracing shows sampled allocations of a given .NET application." );
|
||||
log.info( "Usage: Tracing [-a (all allocations)] [-c (sort by count instead of default by size)] [-t <type count (instead of 3 types by default)>]" );
|
||||
log.info( " Ex: Tracing -t -1 (all types sampled allocations sorted by size)" );
|
||||
log.info( " Ex: Tracing -c -t 10 (allocations for top 10 types sorted by count)" );
|
||||
log.info( $"" );
|
||||
}
|
||||
}
|
||||
}
|
||||
838
ser/XmlSer.cs
838
ser/XmlSer.cs
@ -1,838 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Xml;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Linq;
|
||||
using System.Collections.Immutable;
|
||||
using System.Net.Sockets;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace ser;
|
||||
|
||||
#region Attributes & Enums (Mostly unchanged, ensure these exist)
|
||||
|
||||
/*
|
||||
|
||||
public CoolClass
|
||||
{
|
||||
private string Rare = "Rare Change";
|
||||
private float TestF = 1.0f;
|
||||
}
|
||||
|
||||
public class Bag
|
||||
{
|
||||
public string Owner { get; set; }
|
||||
public CoolClass Cool { get; set; } = new();
|
||||
}
|
||||
public class BasicTest
|
||||
{
|
||||
public Bag MyBag { get; set; } = new();
|
||||
}
|
||||
|
||||
<root>
|
||||
<MyBag Owner="John" Cool.Rare="Changed" >
|
||||
<Cool TestF="2.5" />
|
||||
</MyBag>
|
||||
</root>
|
||||
|
||||
|
||||
|
||||
public abstract class Item
|
||||
{
|
||||
public int Id { get; set; }
|
||||
}
|
||||
|
||||
public class Key : Item
|
||||
{
|
||||
public int Code { get; set; }
|
||||
}
|
||||
|
||||
public class Orb : Item
|
||||
{
|
||||
private float Power = 1.0f;
|
||||
public bool IsDark = true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
||||
public interface I_Serialize
|
||||
{
|
||||
void OnSerialize() { }
|
||||
object OnDeserialize( object enclosing ) => this;
|
||||
}
|
||||
|
||||
|
||||
[Flags]
|
||||
public enum Types
|
||||
{
|
||||
Fields = 0b_0001,
|
||||
Props = 0b_0010,
|
||||
Implied = 0b_0100,
|
||||
Explicit = 0b_1000,
|
||||
|
||||
|
||||
None = 0b_0000,
|
||||
Default = Fields,
|
||||
All = Fields | Props,
|
||||
}
|
||||
|
||||
public class Ser : Attribute
|
||||
{
|
||||
public Types Types { get; set; } = Types.Default;
|
||||
}
|
||||
|
||||
public class Do : Attribute
|
||||
{
|
||||
}
|
||||
|
||||
public class Dont : Attribute
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public class ChildAttribute : Attribute
|
||||
{
|
||||
public string[] Values { get; private set; }
|
||||
|
||||
public ChildAttribute( params string[] values )
|
||||
{
|
||||
this.Values = values;
|
||||
}
|
||||
}
|
||||
|
||||
public class ChildFieldsAttribute : ChildAttribute
|
||||
{
|
||||
public ChildFieldsAttribute( params string[] values ) : base( values ) { }
|
||||
}
|
||||
|
||||
public class ChildPropsAttribute : ChildAttribute
|
||||
{
|
||||
public ChildPropsAttribute( params string[] values ) : base( values ) { }
|
||||
}
|
||||
|
||||
|
||||
|
||||
public interface ITypeHandler
|
||||
{
|
||||
bool CanHandle( TypeInfo typeInfo, XmlElement? elem = null ); // Elem needed for Deser
|
||||
void WriteXml( XmlSer xml, XmlWriter writer, object? obj, string name, Type memberType, bool forceType );
|
||||
object? ReadXml( XmlSer xml, XmlElement elem, Type expectedType, object? existing );
|
||||
}
|
||||
|
||||
|
||||
|
||||
// --- Enums & Records (Slightly adjusted/renamed) ---
|
||||
public enum Datastructure { Tree, Graph }
|
||||
public enum BackingFieldNaming { Short, Regular }
|
||||
public enum POD { Attributes, Elements }
|
||||
public record struct TypeProxy( Func<object, string> fnSer, Func<string, string, object> fnDes );
|
||||
|
||||
public record XmlCfg : io.Recorded<XmlCfg>
|
||||
{
|
||||
public bool Verbose { get; init; } = false;
|
||||
public Datastructure Structure { get; init; } = Datastructure.Tree;
|
||||
public int Version { get; init; } = 2;
|
||||
public ImmutableDictionary<Type, TypeProxy> Proxies { get; init; } = ImmutableDictionary<Type, TypeProxy>.Empty;
|
||||
public BackingFieldNaming Naming { get; init; } = BackingFieldNaming.Short;
|
||||
public POD POD { get; init; } = POD.Attributes;
|
||||
public ser.Types TypesDefault { get; init; } = ser.Types.Fields;
|
||||
public static XmlCfg Default { get; } = new XmlCfg();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reflection & Metadata Cache
|
||||
|
||||
public record DependentMember(
|
||||
string Name, //Prop or field name
|
||||
TypeInfo Enclosing,
|
||||
MemberInfo EnclosingMember
|
||||
);
|
||||
|
||||
public enum MemberMetaType
|
||||
{
|
||||
Invalid,
|
||||
Simple,
|
||||
Composite,
|
||||
}
|
||||
|
||||
public record MemberMeta(
|
||||
//Base type.
|
||||
Type Type,
|
||||
MemberInfo Info,
|
||||
string XmlName,
|
||||
Func<object, object?> GetValue,
|
||||
Action<object, object?> SetValue,
|
||||
bool IsPodAttribute,
|
||||
bool HasDo,
|
||||
bool HasDont
|
||||
);
|
||||
|
||||
public record TypeInfo(
|
||||
Type Type,
|
||||
List<MemberMeta> Members,
|
||||
bool IsISerializable,
|
||||
bool IsImm,
|
||||
bool IsProxy,
|
||||
TypeProxy? ProxyDef
|
||||
);
|
||||
|
||||
public class TypeMetaCache
|
||||
{
|
||||
private readonly ConcurrentDictionary<Type, TypeInfo> _cache = new();
|
||||
private readonly XmlCfg _cfg;
|
||||
|
||||
public TypeMetaCache( XmlCfg cfg ) => _cfg = cfg;
|
||||
|
||||
public TypeInfo Get( Type type )
|
||||
{
|
||||
// Expanded in anticpation of more complex ways to specify saves/loads
|
||||
|
||||
if( _cache.TryGetValue( type, out var ti ) )
|
||||
return ti;
|
||||
|
||||
var children = new HashSet<string>();
|
||||
|
||||
var tiNew = BuildTypeInfo( type, children );
|
||||
_cache.AddOrUpdate( type, tiNew, ( t, ti ) => tiNew );
|
||||
|
||||
return tiNew;
|
||||
}
|
||||
|
||||
// Helper to create accessors (using standard reflection - can be optimized)
|
||||
private static Func<object, object?> CreateGetter( MemberInfo mi )
|
||||
{
|
||||
if( mi is FieldInfo fi )
|
||||
return fi.GetValue;
|
||||
if( mi is PropertyInfo pi && pi.CanRead )
|
||||
return pi.GetValue;
|
||||
return _ => null;
|
||||
}
|
||||
|
||||
private static Action<object, object?> CreateSetter( MemberInfo mi )
|
||||
{
|
||||
if( mi is FieldInfo fi )
|
||||
return fi.SetValue;
|
||||
if( mi is PropertyInfo pi && pi.CanWrite )
|
||||
return pi.SetValue;
|
||||
return ( _, _ ) => { };
|
||||
}
|
||||
|
||||
|
||||
// Helper to create accessors (using standard reflection - can be optimized)
|
||||
private static Func<object, object?> CreateGetter( MemberInfo mi, Func<object, object?> getter )
|
||||
{
|
||||
if( mi is FieldInfo fi )
|
||||
{
|
||||
var innerGet = fi.GetValue;
|
||||
|
||||
return obj => innerGet( getter( obj ) );
|
||||
}
|
||||
|
||||
if( mi is PropertyInfo pi && pi.CanRead )
|
||||
{
|
||||
Func<object, object?> innerGet = pi.GetValue;
|
||||
|
||||
return obj => innerGet( getter( obj ) );
|
||||
}
|
||||
|
||||
// return pi.GetValue;
|
||||
|
||||
|
||||
return _ => null;
|
||||
}
|
||||
|
||||
private static Action<object, object?> CreateSetter( MemberInfo mi, Func<object, object?> getter )
|
||||
{
|
||||
if( mi is FieldInfo fi )
|
||||
{
|
||||
Action<object, object?> innerSet = fi.SetValue;
|
||||
|
||||
return ( obj, value ) => innerSet( getter( obj ), value );
|
||||
}
|
||||
|
||||
|
||||
if( mi is PropertyInfo pi && pi.CanWrite )
|
||||
{
|
||||
Action<object, object?> innerSet = pi.SetValue;
|
||||
|
||||
//return innerSet;
|
||||
|
||||
//var innerSetType = innerSet.GetType();
|
||||
//var isArgs = innerSetType.Metho
|
||||
|
||||
|
||||
return ( obj, value ) =>
|
||||
{
|
||||
var leaf = getter( obj );
|
||||
innerSet( leaf, value );
|
||||
};
|
||||
}
|
||||
|
||||
return ( _, _ ) => { };
|
||||
}
|
||||
|
||||
|
||||
public void AddType( Type type, params string[] children )
|
||||
{
|
||||
var hashChildren = new HashSet<string>( children );
|
||||
|
||||
BuildTypeInfo( type, hashChildren );
|
||||
}
|
||||
|
||||
private TypeInfo BuildTypeInfo( Type type, HashSet<string> children )
|
||||
{
|
||||
if( _cfg.Verbose )
|
||||
log.info( $"Building TypeInfo for {type.Name}" );
|
||||
|
||||
var members = new List<MemberMeta>();
|
||||
bool doImpls, doFields, doProps;
|
||||
|
||||
GetFilters( _cfg.TypesDefault, type, out doImpls, out doFields, out doProps );
|
||||
|
||||
|
||||
var isImm = typeof( io.Obj ).IsAssignableFrom( type );
|
||||
|
||||
var typesAtt = type.GetCustomAttribute<ser.Ser>( true );
|
||||
var serTypes = typesAtt?.Types ?? ser.Types.None;
|
||||
|
||||
|
||||
if( doFields || doImpls )
|
||||
{
|
||||
foreach( var fi in refl.GetAllFields( type ) )
|
||||
{
|
||||
ProcessMember( fi, serTypes.HasFlag( ser.Types.Fields ), children, doImpls, isImm, members );
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
if( doProps || doImpls )
|
||||
{
|
||||
foreach( var pi in refl.GetAllProperties( type ) )
|
||||
{
|
||||
ProcessMember( pi, serTypes.HasFlag( ser.Types.Props ), children, doImpls, isImm, members );
|
||||
}
|
||||
}
|
||||
|
||||
var (isProxy, proxyDef) = FindProxy( type );
|
||||
|
||||
//Dont need to sort since we just run through the attributes first, then the nodes
|
||||
//members.Sort( ( a, b ) => ( a.IsPodAttribute.Int < b.IsPodAttribute.Int ) ? 1 : -1);
|
||||
|
||||
return new TypeInfo(
|
||||
type,
|
||||
members,
|
||||
typeof( ISerializable ).IsAssignableFrom( type ) && !typeof( Delegate ).IsAssignableFrom( type ), // Exclude Delegates
|
||||
isImm,
|
||||
isProxy,
|
||||
proxyDef
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
private (bool, TypeProxy?) FindProxy( Type type )
|
||||
{
|
||||
var tryType = type;
|
||||
while( tryType != null && tryType != typeof( object ) )
|
||||
{
|
||||
if( _cfg.Proxies.TryGetValue( tryType, out var proxy ) )
|
||||
{
|
||||
return (true, proxy);
|
||||
}
|
||||
tryType = tryType.BaseType;
|
||||
}
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private void ProcessMember( MemberInfo mi, bool doMemberType, HashSet<string> childrenOverridden, bool doImpls, bool isImm, List<MemberMeta> members )
|
||||
{
|
||||
List<string> children = new( childrenOverridden );
|
||||
var (hasDo, hasDont, hasImpl, propName, serTypess) = GetMemberAttributes( mi, out var actualMiForAtts, children );
|
||||
|
||||
if( hasDont )
|
||||
return;
|
||||
|
||||
// TODO MH Change this to a configurable query(s)
|
||||
if( isImm && ( mi.Name == "MetaStorage" || mi.Name == "Fn" ) )
|
||||
return;
|
||||
|
||||
|
||||
if( mi.GetCustomAttribute<NonSerializedAttribute>( true ) != null )
|
||||
return;
|
||||
|
||||
if( !(
|
||||
hasDo |
|
||||
doMemberType |
|
||||
( hasImpl & children.Any() )
|
||||
) )
|
||||
return;
|
||||
|
||||
|
||||
var miType = ( mi is FieldInfo fi ) ? fi.FieldType : ( (PropertyInfo)mi ).PropertyType; // CHANGED (moved up)
|
||||
|
||||
string name = mi.Name;
|
||||
string finalName = name;
|
||||
|
||||
if( !string.IsNullOrEmpty( propName ) )
|
||||
{
|
||||
finalName = ( _cfg.Naming == BackingFieldNaming.Short ) ? propName : name;
|
||||
}
|
||||
|
||||
finalName = refl.TypeToIdentifier( finalName ); // Ensure XML-safe name
|
||||
|
||||
var overiddenName = false;
|
||||
|
||||
var getter = CreateGetter( mi );
|
||||
var setter = CreateSetter( mi );
|
||||
|
||||
var blankHashSet = new HashSet<string>();
|
||||
|
||||
if( hasImpl && children.Any() )
|
||||
{
|
||||
//List<MemberMeta> specialMembers = new();
|
||||
foreach( var childName in children )
|
||||
{
|
||||
var memberInfoArr = miType.GetMember( childName );
|
||||
var miFinal = memberInfoArr?.FirstOrDefault();
|
||||
if( miFinal == null )
|
||||
continue;
|
||||
|
||||
var dependentType = miFinal is FieldInfo fidd ? fidd.FieldType : ( miFinal as PropertyInfo ).PropertyType;
|
||||
|
||||
|
||||
bool isPod = Type.GetTypeCode( dependentType ) != TypeCode.Object;
|
||||
|
||||
//ProcessMember( miFinal, blankHashSet, doImpls, isImm, specialMembers );
|
||||
|
||||
//First this one. We need the old getter for the setter.
|
||||
setter = CreateSetter( miFinal, getter );
|
||||
|
||||
//Now wrap the getter itself
|
||||
getter = CreateGetter( miFinal, getter );
|
||||
|
||||
var depName = $"{finalName}.{childName}";
|
||||
|
||||
var memberMeta = new MemberMeta(
|
||||
dependentType,
|
||||
miFinal,
|
||||
depName,
|
||||
getter,
|
||||
setter,
|
||||
isPod && _cfg.POD == POD.Attributes,
|
||||
hasDo,
|
||||
hasDont
|
||||
);
|
||||
|
||||
members.Add( memberMeta );
|
||||
|
||||
if( _cfg.Verbose )
|
||||
{
|
||||
log.info( $"{depName} ({mi.Name}) -> {finalName} ({dependentType.Name}) PodAtt: {isPod && _cfg.POD == POD.Attributes}" );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
return;
|
||||
|
||||
/*
|
||||
foreach( var childName in children )
|
||||
{
|
||||
var memberInfoArr = miType.GetMember( childName );
|
||||
var miFinal = memberInfoArr?.FirstOrDefault();
|
||||
if( miFinal != null )
|
||||
{
|
||||
realMemberType = ( miFinal is FieldInfo fin ) ? fin.FieldType : ( miFinal as PropertyInfo ).PropertyType;
|
||||
|
||||
getter = CreateGetter( miFinal, getter );
|
||||
setter = CreateSetter( miFinal, setter );
|
||||
}
|
||||
}
|
||||
//*/
|
||||
}
|
||||
|
||||
{
|
||||
// Simplified POD check
|
||||
bool isPod = Type.GetTypeCode( miType ) != TypeCode.Object && !typeof( IEnumerable ).IsAssignableFrom( miType ) || overiddenName;
|
||||
|
||||
members.Add( new MemberMeta(
|
||||
miType,
|
||||
mi,
|
||||
finalName,
|
||||
getter,
|
||||
setter,
|
||||
isPod && _cfg.POD == POD.Attributes,
|
||||
hasDo,
|
||||
hasDont
|
||||
) );
|
||||
|
||||
if( _cfg.Verbose )
|
||||
{
|
||||
log.info( $"{mi.Name} ({miType.Name}) -> {finalName} ({miType.Name}) PodAtt: {isPod && _cfg.POD == POD.Attributes}" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void ProcessDepedentMember( Type type, HashSet<string> children, List<MemberMeta> members )
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
private (bool hasDo, bool hasDont, bool hasImpl, string propName, ser.Types serTypess) GetMemberAttributes( MemberInfo mi, out MemberInfo actualMi, List<string> children )
|
||||
{
|
||||
actualMi = mi;
|
||||
string propName = "";
|
||||
bool isBacking = mi.Name.StartsWith( "<" ) && mi.Name.EndsWith( "BackingField" );
|
||||
|
||||
var typesAtt = mi.DeclaringType.GetCustomAttribute<ser.Ser>( true );
|
||||
|
||||
var serTypes = typesAtt?.Types ?? ser.Types.None;
|
||||
|
||||
var doImpls = serTypes.HasFlag( ser.Types.Implied );
|
||||
|
||||
var attDo = actualMi.GetCustomAttribute<ser.Do>() != null;
|
||||
var attDont = actualMi.GetCustomAttribute<ser.Dont>() != null;
|
||||
|
||||
|
||||
|
||||
if( isBacking && mi is FieldInfo )
|
||||
{
|
||||
var gtIndex = mi.Name.IndexOf( '>' );
|
||||
propName = mi.Name.Substring( 1, gtIndex - 1 );
|
||||
var propInfo = mi.DeclaringType?.GetProperty( propName );
|
||||
if( propInfo != null )
|
||||
actualMi = propInfo;
|
||||
}
|
||||
|
||||
var attChildren = actualMi.GetCustomAttribute<ser.ChildAttribute>();
|
||||
if( attChildren != null )
|
||||
{
|
||||
children.AddRange( attChildren.Values );
|
||||
}
|
||||
|
||||
return (
|
||||
attDo,
|
||||
attDont,
|
||||
doImpls,
|
||||
propName,
|
||||
|
||||
serTypes
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// --- These helpers are copied/adapted from XmlFormatter2 ---
|
||||
|
||||
private static void GetFilters( ser.Types typesDefault, Type type, out bool doImpls, out bool doFields, out bool doProps )
|
||||
{
|
||||
var typesTodo = type.GetCustomAttribute<ser.Ser>( true )?.Types ?? typesDefault;
|
||||
|
||||
doImpls = typesTodo.HasFlag( ser.Types.Implied );
|
||||
doFields = typesTodo.HasFlag( ser.Types.Fields );
|
||||
doProps = typesTodo.HasFlag( ser.Types.Props );
|
||||
}
|
||||
}
|
||||
|
||||
public class TypeResolver
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, Type?> _cache = new();
|
||||
private readonly Assembly[] _assemblies;
|
||||
private static readonly FormatterConverter _conv = new();
|
||||
|
||||
public TypeResolver()
|
||||
{
|
||||
_assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||
}
|
||||
|
||||
public Type Resolve( XmlElement elem, Type? expectedType )
|
||||
{
|
||||
if( elem.HasAttribute( "_.t" ) )
|
||||
{
|
||||
var typeName = elem.GetAttribute( "_.t" );
|
||||
var resolved = FindType( typeName );
|
||||
if( resolved != null )
|
||||
return resolved;
|
||||
}
|
||||
return expectedType ?? typeof( object ); // Fallback needed
|
||||
}
|
||||
|
||||
public Type? FindType( string typeName )
|
||||
{
|
||||
return _cache.GetOrAdd( typeName, tn =>
|
||||
{
|
||||
// Try direct lookup first (might work for fully qualified)
|
||||
var t = Type.GetType( tn );
|
||||
if( t != null )
|
||||
return t;
|
||||
|
||||
// Then search assemblies
|
||||
foreach( Assembly a in _assemblies )
|
||||
{
|
||||
t = a.GetType( tn );
|
||||
if( t != null )
|
||||
return t;
|
||||
}
|
||||
log.warn( $"Could not resolve type: {tn}" );
|
||||
return null;
|
||||
} );
|
||||
}
|
||||
|
||||
|
||||
public object ConvertSimple( string value, Type type )
|
||||
{
|
||||
if( type.IsEnum )
|
||||
return Enum.Parse( type, value );
|
||||
try
|
||||
{
|
||||
return _conv.Convert( value, type );
|
||||
}
|
||||
catch( Exception ex )
|
||||
{
|
||||
object defaultVal = type.IsValueType ? Activator.CreateInstance( type )! : null!;
|
||||
log.warn( $"Conversion failed for '{value}' to {type.Name}: {ex.Message}. Returning default of {defaultVal}({defaultVal.GetType().Name})." );
|
||||
return defaultVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region XmlSer (Coordinator)
|
||||
|
||||
public class XmlSer // : IFormatter
|
||||
{
|
||||
internal readonly XmlCfg _cfg;
|
||||
internal readonly TypeMetaCache _meta;
|
||||
internal readonly TypeResolver _resolver;
|
||||
private readonly List<ITypeHandler> _handlers;
|
||||
|
||||
// Per-operation state
|
||||
internal ObjectIDGenerator _idGen = new();
|
||||
internal Dictionary<long, object> _processed = new();
|
||||
private string _streamSource = "";
|
||||
|
||||
public XmlSer( XmlCfg? cfg = null, TypeMetaCache metaCache = null )
|
||||
{
|
||||
var isCustomConfig = cfg != null;
|
||||
|
||||
_cfg = cfg ?? XmlCfg.Default;
|
||||
|
||||
if( _cfg.Verbose )
|
||||
{
|
||||
log.info( $"Config:" );
|
||||
log.info( $" {log.var( _cfg.Verbose )}" );
|
||||
log.info( $" {log.var( _cfg.Structure )}" );
|
||||
log.info( $" {log.var( _cfg.Version )}" );
|
||||
log.info( $" {log.var( _cfg.Naming )}" );
|
||||
log.info( $" {log.var( _cfg.POD )}" );
|
||||
log.info( $" {log.var( _cfg.TypesDefault )}" );
|
||||
}
|
||||
|
||||
|
||||
_meta = metaCache ?? new TypeMetaCache( _cfg );
|
||||
|
||||
_resolver = new TypeResolver();
|
||||
|
||||
_handlers = new List<ITypeHandler>
|
||||
{
|
||||
new ProxyHandler(),
|
||||
new ISerializableHandler(),
|
||||
new PrimitiveHandler(),
|
||||
new CollectionHandler(),
|
||||
new ObjectHandler() // Must be last
|
||||
};
|
||||
|
||||
if( _cfg.Verbose )
|
||||
{
|
||||
log.info( $"Handlers in importance..." );
|
||||
foreach( var h in _handlers )
|
||||
{
|
||||
log.info( $" {h.GetType().Name}" );
|
||||
}
|
||||
|
||||
log.high( "XmlSer Initialized." );
|
||||
}
|
||||
}
|
||||
|
||||
internal ITypeHandler GetHandler( Type t, XmlElement? elem = null )
|
||||
{
|
||||
var ti = _meta.Get( t );
|
||||
return _handlers.First( h => h.CanHandle( ti, elem ) );
|
||||
}
|
||||
|
||||
// --- Context Helpers ---
|
||||
internal void WriteTypeAttr( XmlWriter writer, Type memberType, Type actualType )
|
||||
{
|
||||
if( memberType != actualType )
|
||||
{
|
||||
writer.WriteAttributeString( "_.t", actualType.FullName );
|
||||
}
|
||||
}
|
||||
|
||||
internal bool HandleGraphWrite( XmlWriter writer, object obj, out bool first )
|
||||
{
|
||||
first = true;
|
||||
if( _cfg.Structure == Datastructure.Graph )
|
||||
{
|
||||
long id = _idGen.GetId( obj, out first );
|
||||
writer.WriteAttributeString( "ref", id.ToString() );
|
||||
if( first )
|
||||
_processed[id] = obj;
|
||||
}
|
||||
return first || _cfg.Structure == Datastructure.Tree; // Write if first or if Tree
|
||||
}
|
||||
|
||||
internal long TrackIfGraph( object obj, XmlElement elem )
|
||||
{
|
||||
long id = -1;
|
||||
bool first;
|
||||
if( _cfg.Structure == Datastructure.Graph )
|
||||
{
|
||||
id = _idGen.GetId( obj, out first );
|
||||
if( elem.HasAttribute( "ref" ) )
|
||||
{
|
||||
id = long.Parse( elem.GetAttribute( "ref" ) );
|
||||
}
|
||||
if( !_processed.ContainsKey( id ) )
|
||||
{
|
||||
_processed[id] = obj;
|
||||
}
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
|
||||
// --- Deserialization ---
|
||||
public T? Deserialize<T>( Stream stream ) => (T?)Deserialize( stream, typeof( T ) );
|
||||
|
||||
public object? Deserialize( Stream stream, Type? type = null )
|
||||
{
|
||||
_streamSource = stream.ToString() ?? "{null}"; // Basic source, improve as needed
|
||||
_processed.Clear();
|
||||
_idGen = new ObjectIDGenerator();
|
||||
|
||||
using var reader = XmlReader.Create( stream, new XmlReaderSettings { IgnoreWhitespace = true } );
|
||||
XmlDocument doc = new XmlDocument();
|
||||
try
|
||||
{ doc.Load( reader ); }
|
||||
catch( Exception ex ) { log.exception( ex, $"XML Load failed: {ex.Message}" ); return null; }
|
||||
|
||||
if( doc.DocumentElement == null )
|
||||
return null;
|
||||
|
||||
return ReadNode( doc.DocumentElement, type ?? typeof( object ), null );
|
||||
}
|
||||
|
||||
public void DeserializeInto<T>( Stream stream, T obj ) where T : class
|
||||
{
|
||||
_streamSource = stream.ToString() ?? "{null}";
|
||||
_processed.Clear();
|
||||
_idGen = new ObjectIDGenerator();
|
||||
|
||||
using var reader = XmlReader.Create( stream, new XmlReaderSettings { IgnoreWhitespace = true } );
|
||||
XmlDocument doc = new XmlDocument();
|
||||
try
|
||||
{
|
||||
doc.Load( reader );
|
||||
}
|
||||
catch( Exception ex )
|
||||
{
|
||||
log.exception( ex, $"XML Load failed: {ex.Message}" );
|
||||
return;
|
||||
}
|
||||
|
||||
if( doc.DocumentElement == null )
|
||||
return;
|
||||
|
||||
ReadNode( doc.DocumentElement, typeof( T ), obj );
|
||||
}
|
||||
|
||||
internal object? ReadNode( XmlElement elem, Type expectedType, object? existing )
|
||||
{
|
||||
if( elem.HasAttribute( "v" ) && elem.GetAttribute( "v" ) == "null" )
|
||||
return null;
|
||||
|
||||
// 1. Handle refs (if Graph)
|
||||
if( _cfg.Structure == Datastructure.Graph && elem.HasAttribute( "ref" ) )
|
||||
{
|
||||
long id = long.Parse( elem.GetAttribute( "ref" ) );
|
||||
if( _processed.TryGetValue( id, out var obj ) )
|
||||
return obj;
|
||||
}
|
||||
|
||||
// 2. Determine Type & Select Handler
|
||||
var actualType = _resolver.Resolve( elem, expectedType );
|
||||
var ti = _meta.Get( actualType );
|
||||
var handler = _handlers.First( h => h.CanHandle( ti, elem ) );
|
||||
|
||||
// 3. Delegate
|
||||
return handler.ReadXml( this, elem, actualType, existing );
|
||||
}
|
||||
|
||||
// --- Serialization ---
|
||||
public void Serialize( Stream stream, object root )
|
||||
{
|
||||
_processed.Clear();
|
||||
_idGen = new ObjectIDGenerator();
|
||||
|
||||
var settings = new XmlWriterSettings
|
||||
{
|
||||
Indent = true,
|
||||
Encoding = System.Text.Encoding.UTF8, // Use UTF8 for better compatibility
|
||||
OmitXmlDeclaration = true // Often preferred for fragments/storage
|
||||
};
|
||||
|
||||
using var writer = XmlWriter.Create( stream, settings );
|
||||
|
||||
writer.WriteStartDocument();
|
||||
WriteNode( writer, root, "root", root?.GetType() ?? typeof( object ), true ); // Force type on root
|
||||
writer.WriteEndDocument();
|
||||
writer.Flush();
|
||||
}
|
||||
|
||||
internal void WriteNode( XmlWriter writer, object? obj, string name, Type memberType, bool forceType )
|
||||
{
|
||||
if( _cfg.Verbose )
|
||||
log.info( $"Writing {name} ({memberType}) force: {forceType}" );
|
||||
|
||||
if( obj == null )
|
||||
{
|
||||
writer.WriteStartElement( name );
|
||||
writer.WriteAttributeString( "v", "null" );
|
||||
writer.WriteEndElement();
|
||||
return;
|
||||
}
|
||||
|
||||
var actualType = obj.GetType();
|
||||
var ti = _meta.Get( actualType );
|
||||
var handler = _handlers.First( h => h.CanHandle( ti ) );
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
handler.WriteXml( this, writer, obj, name, memberType, forceType || memberType != actualType );
|
||||
}
|
||||
catch( Exception ex )
|
||||
{
|
||||
log.exception( ex, $"{name}({memberType.Name}) forceType: {forceType}" );
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
@ -1,159 +0,0 @@
|
||||
|
||||
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Xml;
|
||||
|
||||
namespace ser;
|
||||
|
||||
|
||||
// --- Primitive Handler ---
|
||||
public partial class PrimitiveHandler : ser.ITypeHandler
|
||||
{
|
||||
public bool CanHandle( TypeInfo ti, XmlElement? elem )
|
||||
{
|
||||
var typeCode = Type.GetTypeCode( ti.Type );
|
||||
var typeNotObject = Type.GetTypeCode( ti.Type ) != TypeCode.Object;
|
||||
//var isString = ti.Type == typeof( string );
|
||||
return typeNotObject;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Proxy Handler ---
|
||||
public partial class ProxyHandler : ITypeHandler
|
||||
{
|
||||
public bool CanHandle( TypeInfo ti, XmlElement? elem ) => ti.IsProxy || ( elem?.HasAttribute( "proxy" ) ?? false );
|
||||
}
|
||||
|
||||
// --- ISerializable Handler ---
|
||||
public partial class ISerializableHandler : ITypeHandler
|
||||
{
|
||||
public bool CanHandle( TypeInfo ti, XmlElement? elem ) => ti.IsISerializable;
|
||||
}
|
||||
|
||||
// --- Collection Handler ---
|
||||
public partial class CollectionHandler : ITypeHandler
|
||||
{
|
||||
public bool CanHandle( TypeInfo ti, XmlElement? elem ) =>
|
||||
typeof( IEnumerable ).IsAssignableFrom( ti.Type );
|
||||
|
||||
private Type GetElementType( Type collectionType )
|
||||
{
|
||||
if( collectionType.IsArray )
|
||||
return collectionType.GetElementType()!;
|
||||
if( collectionType.IsGenericType )
|
||||
{
|
||||
var args = collectionType.GetGenericArguments();
|
||||
if( args.Length == 1 )
|
||||
return args[0];
|
||||
if( args.Length == 2 )
|
||||
return typeof( KeyValuePair<,> ).MakeGenericType( args );
|
||||
}
|
||||
return typeof( object ); // Fallback
|
||||
}
|
||||
|
||||
private object ConvertToFinalCollection( IList list, Type expectedType, Type elemType )
|
||||
{
|
||||
if( expectedType.IsArray )
|
||||
{
|
||||
var arr = Array.CreateInstance( elemType, list.Count );
|
||||
list.CopyTo( arr, 0 );
|
||||
return arr;
|
||||
}
|
||||
if( expectedType.IsGenericType )
|
||||
{
|
||||
var genDef = expectedType.GetGenericTypeDefinition();
|
||||
if( genDef == typeof( ImmutableArray<> ) )
|
||||
{
|
||||
var method = typeof( ImmutableArray ).GetMethods()
|
||||
.First( m => m.Name == "ToImmutableArray" && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.IsGenericType && m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof( IEnumerable<> ) )
|
||||
.MakeGenericMethod( elemType );
|
||||
return method.Invoke( null, new object[] { list } )!;
|
||||
}
|
||||
if( genDef == typeof( ImmutableList<> ) )
|
||||
{
|
||||
var method = typeof( ImmutableList ).GetMethods()
|
||||
.First( m => m.Name == "ToImmutableList" && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.IsGenericType && m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof( IEnumerable<> ) )
|
||||
.MakeGenericMethod( elemType );
|
||||
return method.Invoke( null, new object[] { list } )!;
|
||||
}
|
||||
// Add more immutable/dictionary handlers here (using MakeImmutableDictionary etc.)
|
||||
}
|
||||
return list; // Default to List<T> if no specific match
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Object Handler (Default/Complex) ---
|
||||
public partial class ObjectHandler : ITypeHandler
|
||||
{
|
||||
public bool CanHandle( TypeInfo ti, XmlElement? elem ) => true; // Fallback
|
||||
|
||||
private (XmlNode? source, bool isAttribute) FindValueSource( XmlElement parent, string name )
|
||||
{
|
||||
if( parent.HasAttribute( name ) )
|
||||
{
|
||||
return (parent.Attributes[name], true);
|
||||
}
|
||||
foreach( XmlNode node in parent.ChildNodes )
|
||||
{
|
||||
if( node.NodeType == XmlNodeType.Element && node.Name == name )
|
||||
{
|
||||
return (node, false);
|
||||
}
|
||||
}
|
||||
return (null, false);
|
||||
}
|
||||
|
||||
private bool ShouldSetValue( MemberMeta member, bool isHydrating )
|
||||
{
|
||||
// [Dont] members are filtered out during metadata generation.
|
||||
// If a member is present in the metadata and a value is found in the XML,
|
||||
// we should always set it. This handles both new creation and hydration/merge.
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
|
||||
private (object? obj, long id) GetOrCreateInstance( XmlSer xml, XmlElement elem, Type type, object? existing )
|
||||
{
|
||||
long id = -1;
|
||||
bool first = true;
|
||||
|
||||
// Check existing
|
||||
if( existing != null && type.IsAssignableFrom( existing.GetType() ) )
|
||||
{
|
||||
id = xml._idGen.GetId( existing, out first );
|
||||
return (existing, id);
|
||||
}
|
||||
|
||||
// Create new
|
||||
object? newObj = null;
|
||||
try
|
||||
{
|
||||
if( type.GetConstructor( Type.EmptyTypes ) != null )
|
||||
{
|
||||
newObj = Activator.CreateInstance( type );
|
||||
}
|
||||
else
|
||||
{
|
||||
newObj = FormatterServices.GetUninitializedObject( type );
|
||||
}
|
||||
}
|
||||
catch( Exception ex )
|
||||
{
|
||||
log.exception( ex, $"Failed to create instance of {type.Name}: {ex.Message}" );
|
||||
return (null, -1);
|
||||
}
|
||||
|
||||
id = xml._idGen.GetId( newObj, out first );
|
||||
return (newObj, id);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,188 +0,0 @@
|
||||
|
||||
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Xml;
|
||||
|
||||
namespace ser;
|
||||
|
||||
|
||||
// --- Primitive Handler ---
|
||||
public partial class PrimitiveHandler : ser.ITypeHandler
|
||||
{
|
||||
public object? ReadXml( XmlSer xml, XmlElement elem, Type expectedType, object? existing )
|
||||
{
|
||||
string val = elem.HasAttribute( "v" ) ? elem.GetAttribute( "v" ) : elem.InnerText;
|
||||
if( val == "null" )
|
||||
return null;
|
||||
|
||||
// So this is an interesting one. Why not use the expected type? Well, we know we have
|
||||
// data in the XML, it just wont convert to what we want.
|
||||
return xml._resolver.ConvertSimple( val, expectedType );
|
||||
}
|
||||
}
|
||||
|
||||
// --- Proxy Handler ---
|
||||
public partial class ProxyHandler : ITypeHandler
|
||||
{
|
||||
public object? ReadXml( XmlSer xml, XmlElement elem, Type expectedType, object? existing )
|
||||
{
|
||||
var ti = xml._meta.Get( expectedType ); // Re-get to ensure we have proxy info
|
||||
if( !elem.HasAttribute( "proxy" ) || !ti.ProxyDef.HasValue )
|
||||
{
|
||||
log.warn( $"Proxy read failed for {expectedType.Name}. Fallback needed." );
|
||||
return null; // Should fall back or throw
|
||||
}
|
||||
var proxyVal = elem.GetAttribute( "proxy" );
|
||||
return ti.ProxyDef.Value.fnDes( expectedType.FullName, proxyVal );
|
||||
}
|
||||
}
|
||||
|
||||
// --- ISerializable Handler ---
|
||||
public partial class ISerializableHandler : ITypeHandler
|
||||
{
|
||||
public object? ReadXml( XmlSer xml, XmlElement elem, Type expectedType, object? existing )
|
||||
{
|
||||
// Create/Get instance (needs FormatterServices for ISerializable)
|
||||
object obj = existing ?? FormatterServices.GetUninitializedObject( expectedType );
|
||||
long id = xml.TrackIfGraph( obj, elem ); // Track it
|
||||
|
||||
var serInfo = new SerializationInfo( expectedType, new FormatterConverter() );
|
||||
|
||||
foreach( XmlNode objNode in elem.ChildNodes )
|
||||
{
|
||||
if( objNode is XmlElement childElem )
|
||||
{
|
||||
string childName = childElem.Name;
|
||||
Type? childType = xml._resolver.FindType( childElem.GetAttribute( "_.t" ) );
|
||||
if( childType != null )
|
||||
{
|
||||
var desValue = xml.ReadNode( childElem, childType, null );
|
||||
serInfo.AddValue( childName, desValue, childType );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var context = new StreamingContext( StreamingContextStates.All ); // Or use xml.Context
|
||||
var cons = expectedType.GetConstructor(
|
||||
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
|
||||
null, new[] { typeof( SerializationInfo ), typeof( StreamingContext ) }, null );
|
||||
|
||||
if( cons != null )
|
||||
{
|
||||
cons.Invoke( obj, new object[] { serInfo, context } );
|
||||
}
|
||||
else
|
||||
{
|
||||
log.error( $"ISerializable type {expectedType.Name} lacks the required constructor." );
|
||||
}
|
||||
|
||||
if( obj is IDeserializationCallback cb )
|
||||
cb.OnDeserialization( obj );
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Collection Handler ---
|
||||
public partial class CollectionHandler : ITypeHandler
|
||||
{
|
||||
|
||||
public object? ReadXml( XmlSer xml, XmlElement elem, Type expectedType, object? existing )
|
||||
{
|
||||
// Determine element type
|
||||
Type elemType = GetElementType( expectedType );
|
||||
|
||||
// Create a temporary list
|
||||
var listType = typeof( List<> ).MakeGenericType( elemType );
|
||||
var list = (IList)Activator.CreateInstance( listType )!;
|
||||
|
||||
xml.TrackIfGraph( list, elem ); // Track list if graph
|
||||
|
||||
// Populate the list
|
||||
foreach( XmlNode node in elem.ChildNodes )
|
||||
{
|
||||
if( node is XmlElement childElem )
|
||||
{
|
||||
list.Add( xml.ReadNode( childElem, elemType, null ) );
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to the final expected type (Array, Immutable*, List)
|
||||
return ConvertToFinalCollection( list, expectedType, elemType );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Object Handler (Default/Complex) ---
|
||||
public partial class ObjectHandler : ITypeHandler
|
||||
{
|
||||
public object? ReadXml( XmlSer xml, XmlElement elem, Type expectedType, object? existing )
|
||||
{
|
||||
var actualType = xml._resolver.Resolve( elem, expectedType );
|
||||
var ti = xml._meta.Get( actualType );
|
||||
|
||||
// 1. Get/Create Instance
|
||||
var (obj, _) = GetOrCreateInstance( xml, elem, actualType, existing );
|
||||
if( obj == null )
|
||||
return null;
|
||||
|
||||
// Handle graph refs (if already processed)
|
||||
if( xml._cfg.Structure == Datastructure.Graph && elem.HasAttribute( "ref" ) )
|
||||
{
|
||||
long id = long.Parse( elem.GetAttribute( "ref" ) );
|
||||
if( xml._processed.TryGetValue( id, out var processedObj ) )
|
||||
return processedObj;
|
||||
}
|
||||
|
||||
// Track if it's new
|
||||
xml.TrackIfGraph( obj, elem );
|
||||
|
||||
// 2. Hydrate
|
||||
foreach( var memberMeta in ti.Members )
|
||||
{
|
||||
|
||||
|
||||
|
||||
{
|
||||
var (valueSource, isAttribute) = FindValueSource( elem, memberMeta.XmlName );
|
||||
|
||||
if( valueSource != null )
|
||||
{
|
||||
object? memberValue;
|
||||
object? currentMemberValue = memberMeta.GetValue( obj );
|
||||
|
||||
if( isAttribute )
|
||||
{
|
||||
memberValue = xml._resolver.ConvertSimple( valueSource.Value!, memberMeta.Type );
|
||||
}
|
||||
else // Child Element
|
||||
{
|
||||
memberValue = xml.ReadNode( (XmlElement)valueSource, memberMeta.Type, currentMemberValue );
|
||||
}
|
||||
|
||||
// Set value, respecting ser.Do/ser.Dont and pre-hydration
|
||||
if( ShouldSetValue( memberMeta, existing != null ) )
|
||||
{
|
||||
memberMeta.SetValue( obj, memberValue );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Post-processing
|
||||
if( obj is ser.I_Serialize iSer )
|
||||
obj = iSer.OnDeserialize( null );
|
||||
if( ti.IsImm && obj is io.Obj immObj )
|
||||
return immObj.Record( $"From XML {elem.Name}" );
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,133 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
//using Org.BouncyCastle.Crypto.IO;
|
||||
|
||||
namespace ser;
|
||||
|
||||
|
||||
public record class SimpleImmutable( string Name, int Age ) : io.Timed<SimpleImmutable>;
|
||||
|
||||
static public class Test
|
||||
{
|
||||
|
||||
public class ExternalClass
|
||||
{
|
||||
public string ActualProperty { get; set; } = "test_ActualProperty_set_inline";
|
||||
//public string ActualProperty_NotSerialized { get; set; } = "ActualProperty_NotSerialized";
|
||||
}
|
||||
|
||||
[ser.Ser( Types = ser.Types.Implied )]
|
||||
public partial class LeaveWithExternalClasss
|
||||
{
|
||||
|
||||
//[ser.Do]
|
||||
//public bool _cccwp_doBool = true;
|
||||
|
||||
[ser.ChildPropsAttribute( "ActualProperty" )]
|
||||
public ExternalClass _leaf_external = new();
|
||||
|
||||
//public string _cccwp_doNotSerialize = "test_do_not_serialize";
|
||||
|
||||
}
|
||||
|
||||
[ser.Ser]
|
||||
public class TrunkClass
|
||||
{
|
||||
public LeaveWithExternalClasss _trunk_leaf = new();
|
||||
//public int _chf_test = 10;
|
||||
//private string _chf_priv_string = "test_priv_string";
|
||||
};
|
||||
|
||||
public static void Serialization()
|
||||
{
|
||||
ser.XmlCfg cfg = new()
|
||||
{
|
||||
Verbose = true,
|
||||
};
|
||||
|
||||
ser.TypeMetaCache metaCache = new( cfg );
|
||||
//metaCache.AddType( typeof( ClassContainsClassWithProp ), "ActualProperty" );
|
||||
|
||||
|
||||
|
||||
|
||||
TrunkClass trunk = new()
|
||||
{
|
||||
_trunk_leaf = new()
|
||||
{
|
||||
_leaf_external = new()
|
||||
{
|
||||
ActualProperty = "ActualProperty_set_in_cons"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Debug.Assert( trunk._trunk_leaf._leaf_external.ActualProperty == "ActualProperty_set_in_cons" );
|
||||
|
||||
|
||||
var memStream = new MemoryStream();
|
||||
|
||||
{
|
||||
var xml = new ser.XmlSer( cfg, metaCache );
|
||||
xml.Serialize( memStream, trunk );
|
||||
}
|
||||
|
||||
memStream.Position = 0;
|
||||
|
||||
var strXml = System.Text.Encoding.UTF8.GetString( memStream.ToArray() );
|
||||
|
||||
var badXml = "<root>\n <prop doBool=\"True\" />\n</root>";
|
||||
///*
|
||||
var badXml_02 = @"""<root>
|
||||
<prop doBool=""True"">
|
||||
<propHolder>
|
||||
<ActualProperty ActualProperty=""ActualProperty_set_in_cons"" />
|
||||
<ActualProperty_NotSerialized ActualProperty_NotSerialized=""ActualProperty_NotSerialized"" />
|
||||
</propHolder>
|
||||
<doNotSerialize doNotSerialize=""test_do_not_serialize"" />
|
||||
</prop>
|
||||
</root>""";
|
||||
//*/
|
||||
Debug.Assert( strXml != badXml );
|
||||
|
||||
memStream.Position = 0;
|
||||
|
||||
var classHasFields2 = new TrunkClass();
|
||||
//classHasFields2._chf_prop._cccwp_propHolder.ActualProperty_NotSerialized = "ActualProperty_NotSerialized_set_in_test_01";
|
||||
|
||||
Debug.Assert( trunk._trunk_leaf._leaf_external.ActualProperty == "test_ActualProperty_set_inline" );
|
||||
//Debug.Assert( classHasFields2._chf_prop._cccwp_propHolder.ActualProperty_NotSerialized == "ActualProperty_NotSerialized" );
|
||||
|
||||
|
||||
{
|
||||
var xml = new ser.XmlSer( cfg, metaCache );
|
||||
classHasFields2 = xml.Deserialize<TrunkClass>( memStream );
|
||||
}
|
||||
|
||||
|
||||
Debug.Assert( trunk._trunk_leaf._leaf_external.ActualProperty == "test_ActualProperty_set_inline" );
|
||||
//Debug.Assert( classHasFields2._chf_prop._cccwp_propHolder.ActualProperty_NotSerialized == "ActualProperty_NotSerialized_set_in_test_01" );
|
||||
|
||||
memStream.Position = 0;
|
||||
|
||||
var classHasFields3 = new TrunkClass();
|
||||
|
||||
{
|
||||
var xml = new ser.XmlSer( cfg, metaCache );
|
||||
xml.DeserializeInto( memStream, classHasFields3 );
|
||||
}
|
||||
|
||||
Debug.Assert( trunk._trunk_leaf._leaf_external.ActualProperty == "test_ActualProperty_set_inline" );
|
||||
//Debug.Assert( classHasFields3._chf_prop._cccwp_propHolder.ActualProperty_NotSerialized == "ActualProperty_NotSerialized_set_in_test_01" );
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -1,201 +0,0 @@
|
||||
|
||||
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Xml;
|
||||
|
||||
namespace ser;
|
||||
|
||||
|
||||
// --- Primitive Handler ---
|
||||
public partial class PrimitiveHandler : ser.ITypeHandler
|
||||
{
|
||||
public void WriteXml( XmlSer xml, XmlWriter writer, object? obj, string name, Type memberType, bool forceType )
|
||||
{
|
||||
if( obj == null )
|
||||
{
|
||||
writer.WriteStartElement( name );
|
||||
writer.WriteAttributeString( "v", "null" );
|
||||
writer.WriteEndElement();
|
||||
return;
|
||||
}
|
||||
|
||||
bool writeElements = xml._cfg.POD == POD.Elements || forceType || !( writer is XmlTextWriter );
|
||||
if( !writeElements && writer is XmlTextWriter tw )
|
||||
writeElements = tw.WriteState != WriteState.Element;
|
||||
|
||||
if( writeElements )
|
||||
writer.WriteStartElement( name );
|
||||
|
||||
if( forceType || xml._cfg.POD == POD.Elements )
|
||||
{
|
||||
if( forceType )
|
||||
writer.WriteAttributeString( "_.t", obj.GetType().FullName );
|
||||
|
||||
writer.WriteAttributeString( "v", obj.ToString() );
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteAttributeString( name, obj.ToString() );
|
||||
}
|
||||
|
||||
if( writeElements )
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Proxy Handler ---
|
||||
public partial class ProxyHandler : ITypeHandler
|
||||
{
|
||||
public void WriteXml( XmlSer xml, XmlWriter writer, object? obj, string name, Type memberType, bool forceType )
|
||||
{
|
||||
if( obj == null )
|
||||
{ xml.GetHandler( typeof( object ) ).WriteXml( xml, writer, null, name, memberType, forceType ); return; }
|
||||
|
||||
var ti = xml._meta.Get( obj.GetType() );
|
||||
if( !ti.ProxyDef.HasValue )
|
||||
{ log.error( "Proxy write called without proxy def!" ); return; }
|
||||
|
||||
writer.WriteStartElement( name );
|
||||
var proxyStr = ti.ProxyDef.Value.fnSer( obj );
|
||||
|
||||
// TODO: Allow arbitrary writing here
|
||||
writer.WriteAttributeString( "proxy", proxyStr );
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
}
|
||||
|
||||
// --- ISerializable Handler ---
|
||||
public partial class ISerializableHandler : ITypeHandler
|
||||
{
|
||||
public void WriteXml( XmlSer xml, XmlWriter writer, object? obj, string name, Type memberType, bool forceType )
|
||||
{
|
||||
if( obj == null )
|
||||
{ /* Write null */ return; }
|
||||
if( !( obj is ISerializable serObj ) )
|
||||
{ /* Error */ return; }
|
||||
|
||||
writer.WriteStartElement( name );
|
||||
xml.WriteTypeAttr( writer, memberType, obj.GetType() );
|
||||
|
||||
if( xml.HandleGraphWrite( writer, obj, out bool first ) )
|
||||
{
|
||||
if( first )
|
||||
{
|
||||
var serInfo = new SerializationInfo( obj.GetType(), new FormatterConverter() );
|
||||
var context = new StreamingContext( StreamingContextStates.All );
|
||||
serObj.GetObjectData( serInfo, context );
|
||||
|
||||
foreach( var member in serInfo )
|
||||
{
|
||||
xml.WriteNode( writer, member.Value, refl.TypeToIdentifier( member.Name ), member.ObjectType, true ); // Force type for ISer
|
||||
}
|
||||
}
|
||||
}
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Collection Handler ---
|
||||
public partial class CollectionHandler : ITypeHandler
|
||||
{
|
||||
public void WriteXml( XmlSer xml, XmlWriter writer, object? obj, string name, Type memberType, bool forceType )
|
||||
{
|
||||
if( obj == null )
|
||||
{ /* Write null */ return; }
|
||||
if( !( obj is IEnumerable collection ) )
|
||||
{ /* Error */ return; }
|
||||
|
||||
writer.WriteStartElement( name );
|
||||
xml.WriteTypeAttr( writer, memberType, obj.GetType() );
|
||||
|
||||
if( xml.HandleGraphWrite( writer, obj, out bool first ) )
|
||||
{
|
||||
if( first )
|
||||
{
|
||||
Type elemType = GetElementType( obj.GetType() );
|
||||
int i = 0;
|
||||
foreach( var item in collection )
|
||||
{
|
||||
xml.WriteNode( writer, item, $"i{i++}", elemType, false );
|
||||
}
|
||||
}
|
||||
}
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Object Handler (Default/Complex) ---
|
||||
public partial class ObjectHandler : ITypeHandler
|
||||
{
|
||||
public void WriteXml( XmlSer xml, XmlWriter writer, object? obj, string name, Type memberType, bool forceType )
|
||||
{
|
||||
if( obj == null )
|
||||
{ /* Write null */ return; }
|
||||
|
||||
writer.WriteStartElement( name );
|
||||
xml.WriteTypeAttr( writer, memberType, obj.GetType() );
|
||||
var ti = xml._meta.Get( obj.GetType() );
|
||||
|
||||
if( xml.HandleGraphWrite( writer, obj, out bool first ) )
|
||||
{
|
||||
if( first )
|
||||
{
|
||||
foreach( var memberMeta in ti.Members )
|
||||
{
|
||||
if( !memberMeta.IsPodAttribute )
|
||||
continue;
|
||||
|
||||
var value = memberMeta.GetValue( obj );
|
||||
if( value != null )
|
||||
{
|
||||
try
|
||||
{
|
||||
writer.WriteAttributeString( memberMeta.XmlName, value.ToString() );
|
||||
}
|
||||
catch( Exception ex )
|
||||
{
|
||||
log.exception( ex, $"Writing Att {memberMeta.XmlName} = [{value}]" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
foreach( var memberMeta in ti.Members )
|
||||
{
|
||||
if( memberMeta.IsPodAttribute )
|
||||
continue;
|
||||
|
||||
var value = memberMeta.GetValue( obj );
|
||||
if( value != null )
|
||||
{
|
||||
try
|
||||
{
|
||||
xml.WriteNode( writer, value, memberMeta.XmlName, memberMeta.Type, false );
|
||||
}
|
||||
catch( Exception ex )
|
||||
{
|
||||
log.exception( ex, $"Writing Node {memberMeta.XmlName} = [{value}]" );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
}
|
||||
|
||||
375
srl/srl.Core.cs
375
srl/srl.Core.cs
@ -1,375 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace srl;
|
||||
|
||||
// --- INTERFACES ---
|
||||
|
||||
public interface Driver
|
||||
{
|
||||
// Structural
|
||||
void BeginScope( string name, Type type, int id );
|
||||
void EndScope();
|
||||
void BeginCollection( string name, int count );
|
||||
void EndCollection();
|
||||
|
||||
// Data
|
||||
void WriteAttr( string name, string value ); // Primitives & Compact Strings
|
||||
void WriteRef( string name, int id ); // DAG/Cycle Reference
|
||||
|
||||
// Metadata Hook
|
||||
void OnProp( MemberInfo member, string name );
|
||||
}
|
||||
|
||||
public interface IInput
|
||||
{
|
||||
string? Value { get; }
|
||||
bool IsLeaf { get; }
|
||||
IInput? GetAttr( string name );
|
||||
IInput? GetChild( string name );
|
||||
}
|
||||
|
||||
// --- THE MODEL (Introspection) ---
|
||||
|
||||
public class TypePlan
|
||||
{
|
||||
public string Name;
|
||||
public bool IsCollection;
|
||||
public bool IsHybrid; // True if Type has a StringParser registered
|
||||
public StringParser Parser; // The "Compact" converter
|
||||
public List<PropPlan> Props = new();
|
||||
}
|
||||
|
||||
public class PropPlan
|
||||
{
|
||||
public string Name;
|
||||
public string MemberName;
|
||||
public Type Type;
|
||||
public bool IsSmart; // Primitive/Enum/String
|
||||
public MemberInfo Info;
|
||||
|
||||
// Fast Accessors
|
||||
public Func<object, object?> Getter;
|
||||
public Action<object, object?> Setter;
|
||||
}
|
||||
|
||||
public static class Model
|
||||
{
|
||||
private static Dictionary<Type, TypePlan> _cache = new();
|
||||
|
||||
public static TypePlan Get( Type t )
|
||||
{
|
||||
if( _cache.TryGetValue( t, out var plan ) )
|
||||
return plan;
|
||||
|
||||
plan = new TypePlan { Name = t.Name };
|
||||
|
||||
// 1. Check for Custom Parsers (Hybrid Mode)
|
||||
if( Parsers.TryGet( t, out var parser ) )
|
||||
{
|
||||
plan.IsHybrid = true;
|
||||
plan.Parser = parser;
|
||||
}
|
||||
|
||||
// 2. Check for Collections
|
||||
if( t != typeof( string ) && typeof( IEnumerable ).IsAssignableFrom( t ) )
|
||||
{
|
||||
plan.IsCollection = true;
|
||||
_cache[t] = plan;
|
||||
return plan;
|
||||
}
|
||||
|
||||
// 3. Scan Properties
|
||||
foreach( var p in t.GetProperties( BindingFlags.Public | BindingFlags.Instance ) )
|
||||
{
|
||||
if( Attribute.IsDefined( p, typeof( IgnoreAttribute ) ) )
|
||||
continue;
|
||||
|
||||
var prop = new PropPlan
|
||||
{
|
||||
Name = p.Name, // Default to PascalCase, Drivers can lower if needed
|
||||
MemberName = p.Name,
|
||||
Type = p.PropertyType,
|
||||
Info = p,
|
||||
Getter = ( o ) => p.GetValue( o ),
|
||||
Setter = ( o, v ) => p.SetValue( o, v )
|
||||
};
|
||||
|
||||
// Override name
|
||||
var nameAttr = p.GetCustomAttribute<NameAttribute>();
|
||||
if( nameAttr != null )
|
||||
prop.Name = nameAttr.Name;
|
||||
|
||||
// Is "Smart" (Atomic)?
|
||||
prop.IsSmart = prop.Type.IsPrimitive || prop.Type.IsEnum ||
|
||||
prop.Type == typeof( string ) || prop.Type == typeof( Guid );
|
||||
|
||||
plan.Props.Add( prop );
|
||||
}
|
||||
|
||||
_cache[t] = plan;
|
||||
return plan;
|
||||
}
|
||||
}
|
||||
|
||||
// --- ATTRIBUTES ---
|
||||
public class IgnoreAttribute : Attribute { }
|
||||
public class NameAttribute : Attribute { public string Name; public NameAttribute( string n ) => Name = n; }
|
||||
|
||||
// --- UTILITIES ---
|
||||
|
||||
public struct StringParser
|
||||
{
|
||||
public Func<object, string> To;
|
||||
public Func<string, object> From;
|
||||
}
|
||||
|
||||
public static class Parsers
|
||||
{
|
||||
private static Dictionary<Type, StringParser> _registry = new();
|
||||
|
||||
public static void Register<T>( Func<T, string> to, Func<string, T> from )
|
||||
{
|
||||
_registry[typeof( T )] = new StringParser { To = o => to( (T)o ), From = s => from( s ) };
|
||||
}
|
||||
|
||||
public static bool TryGet( Type t, out StringParser p )
|
||||
{
|
||||
if( _registry.TryGetValue( t, out p ) )
|
||||
return true;
|
||||
// Auto-Discovery could go here (Static Parse methods)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Binder
|
||||
{
|
||||
// Handles "Cool.Rare" dot notation
|
||||
public static void Apply( object root, string path, string value )
|
||||
{
|
||||
var current = root;
|
||||
var parts = path.Split( '.' );
|
||||
|
||||
for( int i = 0; i < parts.Length; i++ )
|
||||
{
|
||||
var part = parts[i];
|
||||
var isLast = i == parts.Length - 1;
|
||||
var plan = Model.Get( current.GetType() );
|
||||
|
||||
// Case-Insensitive Match
|
||||
var prop = plan.Props.Find( p => p.Name.Equals( part, StringComparison.OrdinalIgnoreCase ) );
|
||||
if( prop == null )
|
||||
return;
|
||||
|
||||
if( isLast )
|
||||
{
|
||||
var val = ParseUtils.Convert( value, prop.Type );
|
||||
prop.Setter( current, val );
|
||||
}
|
||||
else
|
||||
{
|
||||
var next = prop.Getter( current );
|
||||
if( next == null )
|
||||
{
|
||||
next = Activator.CreateInstance( prop.Type );
|
||||
prop.Setter( current, next );
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class ParseUtils
|
||||
{
|
||||
public static object Convert( string raw, Type t )
|
||||
{
|
||||
if( t == typeof( string ) )
|
||||
return raw;
|
||||
if( t == typeof( int ) )
|
||||
return int.Parse( raw );
|
||||
if( t == typeof( float ) )
|
||||
return float.Parse( raw.Replace( "f", "" ) );
|
||||
if( t == typeof( bool ) )
|
||||
return bool.Parse( raw );
|
||||
if( t.IsEnum )
|
||||
return Enum.Parse( t, raw );
|
||||
// Fallback to TypeConverter
|
||||
var cv = TypeDescriptor.GetConverter( t );
|
||||
if( cv != null && cv.CanConvertFrom( typeof( string ) ) )
|
||||
return cv.ConvertFrom( raw );
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void CopyFields( object src, object dst )
|
||||
{
|
||||
foreach( var p in src.GetType().GetProperties() )
|
||||
if( p.CanRead && p.CanWrite )
|
||||
p.SetValue( dst, p.GetValue( src ) );
|
||||
}
|
||||
}
|
||||
|
||||
// --- THE ENGINE (Walker & Loader) ---
|
||||
|
||||
public static class Walker
|
||||
{
|
||||
public class Context
|
||||
{
|
||||
private Dictionary<object, int> _seen = new( ReferenceEqualityComparer.Instance );
|
||||
private int _nextId = 1;
|
||||
public (int, bool) GetId( object o )
|
||||
{
|
||||
if( _seen.TryGetValue( o, out var id ) )
|
||||
return (id, false);
|
||||
_seen[o] = _nextId;
|
||||
return (_nextId++, true);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Serialize( object root, Driver d )
|
||||
{
|
||||
if( root == null )
|
||||
return;
|
||||
SerializeRecursive( root, d, new Context(), "root", null );
|
||||
}
|
||||
|
||||
private static void SerializeRecursive( object obj, Driver d, Context ctx, string name, MemberInfo? member )
|
||||
{
|
||||
if( obj == null )
|
||||
return;
|
||||
|
||||
Type type = obj.GetType();
|
||||
var plan = Model.Get( type );
|
||||
|
||||
// STRATEGY 1: COMPACT (Hybrid Parser)
|
||||
if( plan.IsHybrid )
|
||||
{
|
||||
if( member != null )
|
||||
d.OnProp( member, name );
|
||||
d.WriteAttr( name, plan.Parser.To( obj ) );
|
||||
return;
|
||||
}
|
||||
|
||||
// STRATEGY 2: DAG CHECK
|
||||
bool isRef = !type.IsValueType && type != typeof( string );
|
||||
if( isRef )
|
||||
{
|
||||
var (id, isNew) = ctx.GetId( obj );
|
||||
if( !isNew )
|
||||
{ d.WriteRef( name, id ); return; }
|
||||
if( member != null )
|
||||
d.OnProp( member, name );
|
||||
d.BeginScope( name, type, id );
|
||||
}
|
||||
else
|
||||
{
|
||||
if( member != null )
|
||||
d.OnProp( member, name );
|
||||
d.BeginScope( name, type, 0 );
|
||||
}
|
||||
|
||||
// STRATEGY 3: COLLECTIONS
|
||||
if( plan.IsCollection )
|
||||
{
|
||||
var list = (IEnumerable)obj;
|
||||
int count = 0; // Simple count (could optimize for ICollection)
|
||||
foreach( var _ in list )
|
||||
count++;
|
||||
|
||||
d.BeginCollection( name, count );
|
||||
foreach( var item in list )
|
||||
SerializeRecursive( item, d, ctx, "item", null );
|
||||
d.EndCollection();
|
||||
}
|
||||
else
|
||||
{
|
||||
// STRATEGY 4: STANDARD OBJECT
|
||||
foreach( var prop in plan.Props )
|
||||
{
|
||||
var val = prop.Getter( obj );
|
||||
if( prop.IsSmart )
|
||||
{
|
||||
d.OnProp( prop.Info, prop.Name );
|
||||
d.WriteAttr( prop.Name, val?.ToString() ?? "" );
|
||||
}
|
||||
else
|
||||
{
|
||||
if( val != null )
|
||||
SerializeRecursive( val, d, ctx, prop.Name, prop.Info );
|
||||
}
|
||||
}
|
||||
}
|
||||
d.EndScope();
|
||||
}
|
||||
}
|
||||
|
||||
public static class Loader
|
||||
{
|
||||
public static void Load( object target, IInput input )
|
||||
{
|
||||
if( target == null || input == null )
|
||||
return;
|
||||
var plan = Model.Get( target.GetType() );
|
||||
|
||||
// 1. HYBRID PARSE (Compact String)
|
||||
// If we have a parser AND input is just a value "1,1"
|
||||
if( plan.IsHybrid && input.IsLeaf && !string.IsNullOrWhiteSpace( input.Value ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
var newObj = plan.Parser.From( input.Value );
|
||||
ParseUtils.CopyFields( newObj, target );
|
||||
return;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// 2. STRUCTURAL MAP
|
||||
foreach( var prop in plan.Props )
|
||||
{
|
||||
// Look for Attribute OR Child Element
|
||||
var sub = input.GetAttr( prop.Name ) ?? input.GetChild( prop.Name );
|
||||
|
||||
// Look for Dot Notation (e.g. "Pos.X") in attributes
|
||||
// (Note: This simple loop doesn't scan ALL attrs for dots,
|
||||
// it relies on the caller or specific recursive logic.
|
||||
// For full dot support on root, we need to iterate input attributes if possible.
|
||||
// But 'Binder' below handles it if we pass the specific attr key).
|
||||
|
||||
if( sub != null )
|
||||
{
|
||||
if( prop.IsSmart )
|
||||
{
|
||||
if( sub.Value != null )
|
||||
prop.Setter( target, ParseUtils.Convert( sub.Value, prop.Type ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
var child = prop.Getter( target );
|
||||
if( child == null )
|
||||
{
|
||||
child = Activator.CreateInstance( prop.Type );
|
||||
prop.Setter( target, child );
|
||||
}
|
||||
Load( child, sub );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to scan all attributes on an element for "Cool.Rare" patterns
|
||||
public static void LoadDotNotations( object target, IEnumerable<(string k, string v)> attrs )
|
||||
{
|
||||
foreach( var (k, v) in attrs )
|
||||
{
|
||||
if( k.Contains( '.' ) )
|
||||
Binder.Apply( target, k, v );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,47 +0,0 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace srl.Debug;
|
||||
|
||||
public class DebugDriver : Driver
|
||||
{
|
||||
private StringBuilder _sb = new();
|
||||
private int _indent = 0;
|
||||
|
||||
public override string ToString() => _sb.ToString();
|
||||
|
||||
private void Line( string s ) => _sb.AppendLine( new string( ' ', _indent * 2 ) + s );
|
||||
|
||||
public void BeginScope( string name, Type type, int id )
|
||||
{
|
||||
var refStr = id > 0 ? $" #{id}" : "";
|
||||
Line( $"[{name}] <{type.Name}>{refStr}" );
|
||||
_indent++;
|
||||
}
|
||||
|
||||
public void EndScope() => _indent--;
|
||||
|
||||
public void BeginCollection( string name, int count )
|
||||
{
|
||||
Line( $"[{name}] (Count: {count})" );
|
||||
_indent++;
|
||||
}
|
||||
|
||||
public void EndCollection() => _indent--;
|
||||
|
||||
public void WriteAttr( string name, string value )
|
||||
{
|
||||
Line( $"{name} = {value}" );
|
||||
}
|
||||
|
||||
public void WriteRef( string name, int id )
|
||||
{
|
||||
Line( $"{name} -> See #{id}" );
|
||||
}
|
||||
|
||||
public void OnProp( MemberInfo m, string name )
|
||||
{
|
||||
// Could log attributes here, e.g. [Tooltip]
|
||||
}
|
||||
}
|
||||
131
srl/srl.Xml.cs
131
srl/srl.Xml.cs
@ -1,131 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace srl.Xml;
|
||||
|
||||
// --- INPUT ADAPTER ---
|
||||
public class XmlAdapter : IInput
|
||||
{
|
||||
private XElement _e;
|
||||
private XAttribute _a;
|
||||
|
||||
public XmlAdapter( XElement e ) => _e = e;
|
||||
public XmlAdapter( XAttribute a ) => _a = a;
|
||||
|
||||
public string? Value => _e?.Value ?? _a?.Value;
|
||||
public bool IsLeaf => _a != null || ( _e != null && !_e.HasElements );
|
||||
|
||||
public IInput? GetAttr( string name )
|
||||
{
|
||||
if( _e == null )
|
||||
return null;
|
||||
// Case-Insensitive search
|
||||
var a = _e.Attribute( name ) ?? _e.Attribute( name.ToLower() );
|
||||
return a != null ? new XmlAdapter( a ) : null;
|
||||
}
|
||||
|
||||
public IInput? GetChild( string name )
|
||||
{
|
||||
if( _e == null )
|
||||
return null;
|
||||
|
||||
var c = _e.Element( name ) ?? _e.Element( name.ToLower() );
|
||||
|
||||
return c != null ? new XmlAdapter( c ) : null;
|
||||
}
|
||||
|
||||
// Extra helper for Dot Notation scanning
|
||||
public IEnumerable<(string, string)> GetAllAttrs()
|
||||
{
|
||||
if( _e == null )
|
||||
yield break;
|
||||
foreach( var a in _e.Attributes() )
|
||||
yield return (a.Name.LocalName, a.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// --- OUTPUT DRIVER ---
|
||||
public class XmlDriver : Driver
|
||||
{
|
||||
private Stack<XElement> _stack = new();
|
||||
private XDocument _doc;
|
||||
|
||||
public XDocument Document => _doc;
|
||||
|
||||
public XmlDriver()
|
||||
{
|
||||
_doc = new XDocument();
|
||||
}
|
||||
|
||||
public void BeginScope( string name, Type type, int id )
|
||||
{
|
||||
var el = new XElement( name );
|
||||
|
||||
// Polymorphism Metadata (if needed, e.g. <Item type="Sword">)
|
||||
// el.Add(new XAttribute("_type", type.Name));
|
||||
|
||||
if( id > 0 )
|
||||
el.Add( new XAttribute( "_id", id ) ); // DAG ID
|
||||
|
||||
if( _stack.Count > 0 )
|
||||
_stack.Peek().Add( el );
|
||||
else
|
||||
_doc.Add( el );
|
||||
|
||||
_stack.Push( el );
|
||||
}
|
||||
|
||||
public void EndScope() => _stack.Pop();
|
||||
|
||||
public void BeginCollection( string name, int count )
|
||||
{
|
||||
// XML doesn't strictly need array wrappers, but it helps structure
|
||||
// We use the same BeginScope logic effectively
|
||||
var el = new XElement( name, new XAttribute( "_count", count ) );
|
||||
if( _stack.Count > 0 )
|
||||
_stack.Peek().Add( el );
|
||||
_stack.Push( el );
|
||||
}
|
||||
|
||||
public void EndCollection() => _stack.Pop();
|
||||
|
||||
public void WriteAttr( string name, string value )
|
||||
{
|
||||
if( _stack.Count == 0 )
|
||||
return;
|
||||
_stack.Peek().Add( new XAttribute( name, value ) );
|
||||
}
|
||||
|
||||
public void WriteRef( string name, int id )
|
||||
{
|
||||
var el = new XElement( name, new XAttribute( "_ref", id ) );
|
||||
_stack.Peek().Add( el );
|
||||
}
|
||||
|
||||
public void OnProp( MemberInfo m, string name ) { /* Optional: Write tooltips/comments */ }
|
||||
}
|
||||
|
||||
// --- FACADE ---
|
||||
public static class XmlSerializer
|
||||
{
|
||||
public static string Serialize( object obj )
|
||||
{
|
||||
var driver = new XmlDriver();
|
||||
srl.Walker.Serialize( obj, driver );
|
||||
return driver.Document.ToString();
|
||||
}
|
||||
|
||||
public static void Deserialize( object root, string xml )
|
||||
{
|
||||
var doc = XDocument.Parse( xml );
|
||||
var adapter = new XmlAdapter( doc.Root );
|
||||
|
||||
// 1. Standard Load
|
||||
srl.Loader.Load( root, adapter );
|
||||
|
||||
// 2. Dot Notation Pass (MyBag Cool.Rare="Changed")
|
||||
srl.Loader.LoadDotNotations( root, adapter.GetAllAttrs() );
|
||||
}
|
||||
}
|
||||
16
task/Task.cs
16
task/Task.cs
@ -1,16 +0,0 @@
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Diagnostics;
|
||||
|
||||
|
||||
namespace lib;
|
||||
|
||||
|
||||
static public class Task
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
125
tests/Tests.cs
125
tests/Tests.cs
@ -1,125 +0,0 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
namespace test;
|
||||
|
||||
|
||||
public record class SimpleImmutable( string Name, int Age ) : io.Timed<SimpleImmutable>;
|
||||
|
||||
static public class XmlFormatter2
|
||||
{
|
||||
|
||||
public class ClassWithProperties
|
||||
{
|
||||
public string ActualProperty { get; set; } = "test_ActualProperty_set_inline";
|
||||
public string ActualProperty_NotSerialized { get; set; } = "ActualProperty_NotSerialized";
|
||||
}
|
||||
|
||||
[ser.Ser( Types = ser.Types.Implied )]
|
||||
public partial class ClassContainsClassWithProp
|
||||
{
|
||||
|
||||
[ser.Do]
|
||||
public bool doBool = true;
|
||||
|
||||
[ser.ChildPropsAttribute( "ActualProperty" )]
|
||||
public ClassWithProperties propHolder = new();
|
||||
|
||||
public string doNotSerialize = "test_do_not_serialize";
|
||||
|
||||
}
|
||||
|
||||
[ser.Ser]
|
||||
public class ClassHasFields
|
||||
{
|
||||
public ClassContainsClassWithProp prop = new();
|
||||
};
|
||||
|
||||
public static void Serialization()
|
||||
{
|
||||
|
||||
lib.XmlFormatter2Cfg cfg = new()
|
||||
{
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
ClassHasFields classHasFields = new()
|
||||
{
|
||||
prop = new()
|
||||
{
|
||||
propHolder = new()
|
||||
{
|
||||
ActualProperty = "ActualProperty_set_in_cons"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Debug.Assert( classHasFields.prop.propHolder.ActualProperty == "ActualProperty_set_in_cons" );
|
||||
|
||||
|
||||
var memStream = new MemoryStream();
|
||||
|
||||
{
|
||||
var xml = new lib.XmlFormatter2( cfg );
|
||||
xml.Serialize( memStream, classHasFields );
|
||||
}
|
||||
|
||||
memStream.Position = 0;
|
||||
|
||||
var strXml = System.Text.Encoding.UTF8.GetString( memStream.ToArray() );
|
||||
|
||||
var badXml = "<root>\n <prop doBool=\"True\" />\n</root>";
|
||||
/*
|
||||
<root _.t="test.XmlFormatter2+ClassHasFields" _.version.="2">
|
||||
<prop doBool="True">
|
||||
<propHolder ActualProperty="ActualProperty_set_in_cons" ActualProperty_NotSerialized="ActualProperty_NotSerialized" ActualProperty="ActualProperty_set_in_cons" />
|
||||
</prop>
|
||||
</root>*/
|
||||
Debug.Assert( strXml != badXml );
|
||||
|
||||
memStream.Position = 0;
|
||||
|
||||
var classHasFields2 = new ClassHasFields();
|
||||
classHasFields2.prop.propHolder.ActualProperty_NotSerialized = "ActualProperty_NotSerialized_set_in_test_01";
|
||||
|
||||
Debug.Assert( classHasFields2.prop.propHolder.ActualProperty == "test_ActualProperty_set_inline" );
|
||||
Debug.Assert( classHasFields2.prop.propHolder.ActualProperty_NotSerialized == "ActualProperty_NotSerialized" );
|
||||
|
||||
|
||||
{
|
||||
var xml = new lib.XmlFormatter2( cfg );
|
||||
classHasFields2 = xml.Deserialize<ClassHasFields>( memStream );
|
||||
}
|
||||
|
||||
|
||||
Debug.Assert( classHasFields2.prop.propHolder.ActualProperty == "ActualProperty_set_in_cons" );
|
||||
Debug.Assert( classHasFields2.prop.propHolder.ActualProperty_NotSerialized == "ActualProperty_NotSerialized_set_in_test_01" );
|
||||
|
||||
memStream.Position = 0;
|
||||
|
||||
var classHasFields3 = new ClassHasFields();
|
||||
|
||||
{
|
||||
var xml = new lib.XmlFormatter2( cfg );
|
||||
xml.DeserializeInto( memStream, classHasFields3 );
|
||||
}
|
||||
|
||||
Debug.Assert( classHasFields3.prop.propHolder.ActualProperty == "ActualProperty_set_in_cons" );
|
||||
Debug.Assert( classHasFields3.prop.propHolder.ActualProperty_NotSerialized == "ActualProperty_NotSerialized_set_in_test_01" );
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user