using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
class TypingBenchmark {
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool AttachConsole(uint pid);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool FreeConsole();
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
static extern IntPtr CreateFile(string name, uint access, uint share, IntPtr sa, uint disp, uint flags, IntPtr tmpl);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool WriteConsoleInput(IntPtr h, INPUT_RECORD[] buf, uint len, out uint written);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool CloseHandle(IntPtr h);
[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr GetStdHandle(int nStdHandle);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
static extern bool ReadConsoleOutputCharacter(IntPtr hConsoleOutput, StringBuilder lpCharacter,
uint nLength, COORD dwReadCoord, out uint lpNumberOfCharsRead);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool GetConsoleScreenBufferInfo(IntPtr hConsoleOutput, out CONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferInfo);
[StructLayout(LayoutKind.Explicit)]
struct INPUT_RECORD {
[FieldOffset(0)] public ushort EventType;
[FieldOffset(4)] public KEY_EVENT_RECORD KeyEvent;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct KEY_EVENT_RECORD {
public int bKeyDown;
public ushort wRepeatCount;
public ushort wVirtualKeyCode;
public ushort wVirtualScanCode;
public char UnicodeChar;
public uint dwControlKeyState;
}
[StructLayout(LayoutKind.Sequential)]
struct COORD {
public short X;
public short Y;
public COORD(short x, short y) { X = x; Y = y; }
}
[StructLayout(LayoutKind.Sequential)]
struct SMALL_RECT {
public short Left, Top, Right, Bottom;
}
[StructLayout(LayoutKind.Sequential)]
struct CONSOLE_SCREEN_BUFFER_INFO {
public COORD dwSize;
public COORD dwCursorPosition;
public ushort wAttributes;
public SMALL_RECT srWindow;
public COORD dwMaximumWindowSize;
}
static volatile bool injectionDone = false;
static volatile int injectedCount = 0;
static void Main(string[] args) {
if (args.Length < 3) {
Console.Error.WriteLine("Usage: typing_benchmark.exe <PID> <text> <interval_ms> [monitor_row] [monitor_col_start]");
Environment.Exit(1);
}
uint pid = uint.Parse(args[0]);
string text = args[1];
int interval = int.Parse(args[2]);
FreeConsole();
if (!AttachConsole(pid)) {
Console.Error.WriteLine("AttachConsole failed: " + Marshal.GetLastWin32Error());
Environment.Exit(2);
}
IntPtr hIn = CreateFile("CONIN$", 0xC0000000u, 3, IntPtr.Zero, 3, 0, IntPtr.Zero);
IntPtr hOut = CreateFile("CONOUT$", 0x80000000u, 3, IntPtr.Zero, 3, 0, IntPtr.Zero);
if (hIn == (IntPtr)(-1) || hOut == (IntPtr)(-1)) {
Console.Error.WriteLine("CreateFile failed: " + Marshal.GetLastWin32Error());
Environment.Exit(3);
}
CONSOLE_SCREEN_BUFFER_INFO csbi;
GetConsoleScreenBufferInfo(hOut, out csbi);
short monitorRow = csbi.dwCursorPosition.Y;
short monitorColStart = csbi.dwCursorPosition.X;
int bufWidth = csbi.dwSize.X;
if (args.Length > 3) monitorRow = short.Parse(args[3]);
if (args.Length > 4) monitorColStart = short.Parse(args[4]);
var samples = new List<long[]>(); var sw = Stopwatch.StartNew();
string initialContent = ReadRow(hOut, monitorRow, monitorColStart, bufWidth);
int baseLen = initialContent.TrimEnd().Length;
var injThread = new Thread(() => {
foreach (char c in text) {
INPUT_RECORD[] recs = new INPUT_RECORD[2];
recs[0].EventType = 1;
recs[0].KeyEvent.bKeyDown = 1;
recs[0].KeyEvent.wRepeatCount = 1;
recs[0].KeyEvent.UnicodeChar = c;
recs[1].EventType = 1;
recs[1].KeyEvent.bKeyDown = 0;
recs[1].KeyEvent.wRepeatCount = 1;
recs[1].KeyEvent.UnicodeChar = c;
uint written;
WriteConsoleInput(hIn, recs, 2, out written);
Interlocked.Increment(ref injectedCount);
if (interval > 0) Thread.Sleep(interval);
}
injectionDone = true;
});
int prevVisibleLen = 0;
long lastChangeMs = 0;
int maxGapMs = 0;
int stallCount = 0;
int burstCount = 0;
long firstCharMs = 0;
long lastCharMs = 0;
injThread.Start();
int timeoutMs = (text.Length * Math.Max(interval, 1)) + 10000;
bool allSeen = false;
while (sw.ElapsedMilliseconds < timeoutMs && !allSeen) {
string visibleText = "";
for (short row = monitorRow; row < monitorRow + 5 && row < csbi.dwSize.Y; row++) {
string rowContent;
if (row == monitorRow) {
rowContent = ReadRow(hOut, row, monitorColStart, bufWidth);
} else {
rowContent = ReadRow(hOut, row, 0, bufWidth);
}
string trimmed = rowContent.TrimEnd();
if (trimmed.Length > 0) {
visibleText += trimmed;
}
}
int curLen = visibleText.Length;
long ts = sw.ElapsedMilliseconds;
if (curLen != prevVisibleLen) {
int delta = curLen - prevVisibleLen;
if (prevVisibleLen > 0) {
int gap = (int)(ts - lastChangeMs);
if (gap > maxGapMs) maxGapMs = gap;
if (gap > 200) stallCount++;
if (delta > 5) burstCount++;
}
if (firstCharMs == 0 && curLen > 0) firstCharMs = ts;
lastCharMs = ts;
lastChangeMs = ts;
samples.Add(new long[] { ts, curLen, delta });
prevVisibleLen = curLen;
}
if (curLen >= text.Length) allSeen = true;
Thread.Sleep(10);
}
injThread.Join(5000);
CloseHandle(hIn);
CloseHandle(hOut);
FreeConsole();
Console.WriteLine("TIMESTAMP_MS,VISIBLE_CHARS,DELTA");
foreach (var s in samples) {
Console.WriteLine(s[0] + "," + s[1] + "," + s[2]);
}
var gaps = new List<int>();
for (int i = 1; i < samples.Count; i++) {
gaps.Add((int)(samples[i][0] - samples[i - 1][0]));
}
gaps.Sort();
int p50 = gaps.Count > 0 ? gaps[gaps.Count / 2] : 0;
int p90 = gaps.Count > 0 ? gaps[(int)(gaps.Count * 0.9)] : 0;
int p99 = gaps.Count > 0 ? gaps[(int)(gaps.Count * 0.99)] : 0;
int avgGap = 0;
if (gaps.Count > 0) {
long sum = 0; foreach (int g in gaps) sum += g; avgGap = (int)(sum / gaps.Count);
}
long renderSpan = lastCharMs - firstCharMs;
long injectTime = text.Length * Math.Max(interval, 1);
Console.WriteLine("SUMMARY chars={0} inject_ms={1} render_ms={2} first_char_ms={3} last_char_ms={4} samples={5} stalls={6} bursts={7} max_gap_ms={8} avg_gap_ms={9} p50_ms={10} p90_ms={11} p99_ms={12} all_seen={13}",
text.Length, injectTime, renderSpan, firstCharMs, lastCharMs,
samples.Count, stallCount, burstCount, maxGapMs, avgGap, p50, p90, p99, allSeen);
}
static string ReadRow(IntPtr hOut, short row, short colStart, int bufWidth) {
int readLen = bufWidth - colStart;
if (readLen <= 0) return "";
var sb = new StringBuilder(readLen);
uint charsRead;
ReadConsoleOutputCharacter(hOut, sb, (uint)readLen, new COORD(colStart, row), out charsRead);
return sb.ToString(0, (int)charsRead);
}
}