using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
class TypingBench {
[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")]
static extern bool CloseHandle(IntPtr h);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool GetConsoleScreenBufferInfo(IntPtr h, out CSBI info);
[DllImport("user32.dll")]
static extern uint MapVirtualKeyW(uint code, uint mapType);
[DllImport("user32.dll")]
static extern short VkKeyScanW(char ch);
const ushort KEY_EVENT = 1;
const uint SHIFT_PRESSED = 0x0010;
const uint LEFT_CTRL_PRESSED = 0x0008;
[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;
}
struct COORD { public short X, Y; }
struct SMALL_RECT { public short Left, Top, Right, Bottom; }
struct CSBI {
public COORD dwSize;
public COORD dwCursorPosition;
public ushort wAttributes;
public SMALL_RECT srWindow;
public COORD dwMaximumWindowSize;
}
static volatile bool injDone = false;
static volatile int injectedCount = 0;
static int CursorLinear(CSBI c) {
return c.dwCursorPosition.Y * c.dwSize.X + c.dwCursorPosition.X;
}
static INPUT_RECORD MakeKey(bool down, ushort vk, char ch, uint ctrl) {
var r = new INPUT_RECORD();
r.EventType = KEY_EVENT;
r.KeyEvent.bKeyDown = down ? 1 : 0;
r.KeyEvent.wRepeatCount = 1;
r.KeyEvent.wVirtualKeyCode = vk;
r.KeyEvent.wVirtualScanCode = (ushort)MapVirtualKeyW(vk, 0);
r.KeyEvent.UnicodeChar = ch;
r.KeyEvent.dwControlKeyState = ctrl;
return r;
}
static void CharToVK(char c, out ushort vk, out uint ctrl) {
ctrl = 0;
if (c >= 'a' && c <= 'z') { vk = (ushort)(0x41 + c - 'a'); }
else if (c >= 'A' && c <= 'Z') { vk = (ushort)(0x41 + c - 'A'); ctrl = SHIFT_PRESSED; }
else if (c >= '0' && c <= '9') { vk = (ushort)(0x30 + c - '0'); }
else if (c == ' ') vk = 0x20;
else if (c == '-') vk = 0xBD;
else if (c == '_') { vk = 0xBD; ctrl = SHIFT_PRESSED; }
else if (c == '.') vk = 0xBE;
else if (c == ',') vk = 0xBC;
else if (c == '/') vk = 0xBF;
else if (c == '\'') vk = 0xDE;
else if (c == '"') { vk = 0xDE; ctrl = SHIFT_PRESSED; }
else if (c == ';') vk = 0xBA;
else if (c == ':') { vk = 0xBA; ctrl = SHIFT_PRESSED; }
else if (c == '!') { vk = 0x31; ctrl = SHIFT_PRESSED; }
else if (c == '?') { vk = 0xBF; ctrl = SHIFT_PRESSED; }
else {
short vks = VkKeyScanW(c);
if (vks != -1) {
vk = (ushort)(vks & 0xFF);
if ((vks & 0x100) != 0) ctrl |= SHIFT_PRESSED;
} else {
vk = 0;
}
}
}
static void InjectChar(IntPtr hIn, char c) {
ushort vk;
uint ctrl;
CharToVK(c, out vk, out ctrl);
var recs = new INPUT_RECORD[2];
recs[0] = MakeKey(true, vk, c, ctrl);
recs[1] = MakeKey(false, vk, c, 0);
uint written;
WriteConsoleInput(hIn, recs, 2, out written);
}
static void InjectCtrlCombo(IntPtr hIn, char letter) {
ushort vk = (ushort)char.ToUpper(letter);
char ctrlChar = (char)(char.ToUpper(letter) - 'A' + 1);
var recs = new INPUT_RECORD[4];
recs[0] = MakeKey(true, 0x11, '\0', LEFT_CTRL_PRESSED);
recs[1] = MakeKey(true, vk, ctrlChar, LEFT_CTRL_PRESSED);
recs[2] = MakeKey(false, vk, ctrlChar, LEFT_CTRL_PRESSED);
recs[3] = MakeKey(false, 0x11, '\0', 0);
uint written;
WriteConsoleInput(hIn, recs, 4, out written);
}
static void InjectEnter(IntPtr hIn) {
var recs = new INPUT_RECORD[2];
recs[0] = MakeKey(true, 0x0D, '\r', 0);
recs[1] = MakeKey(false, 0x0D, '\r', 0);
uint written;
WriteConsoleInput(hIn, recs, 2, out written);
}
static void InjectEscape(IntPtr hIn) {
var recs = new INPUT_RECORD[2];
recs[0] = MakeKey(true, 0x1B, (char)0x1B, 0);
recs[1] = MakeKey(false, 0x1B, (char)0x1B, 0);
uint written;
WriteConsoleInput(hIn, recs, 2, out written);
}
static void Main(string[] args) {
if (args.Length < 4) {
Console.Error.WriteLine("Usage: typing_bench.exe <PID> <text> <intra_ms> <inter_ms>");
Console.Error.WriteLine(" intra_ms: delay between chars within a word");
Console.Error.WriteLine(" inter_ms: delay between words (after space)");
Environment.Exit(1);
}
uint pid = uint.Parse(args[0]);
string text = args[1];
int intraMs = int.Parse(args[2]);
int interMs = int.Parse(args[3]);
bool clearOnly = args.Length > 4 && args[4] == "clear";
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");
Environment.Exit(3);
}
if (clearOnly) {
InjectEscape(hIn);
Thread.Sleep(50);
foreach (char c in "cls") { InjectChar(hIn, c); Thread.Sleep(30); }
Thread.Sleep(50);
InjectEnter(hIn);
Thread.Sleep(500);
CloseHandle(hIn); CloseHandle(hOut); FreeConsole();
return;
}
CSBI csbi;
GetConsoleScreenBufferInfo(hOut, out csbi);
int baseOffset = CursorLinear(csbi);
var timestamps = new List<long>(); var positions = new List<int>(); var deltas = new List<int>(); var gaps = new List<int>();
int prevOffset = baseOffset;
long lastChangeMs = 0;
long firstChangeMs = 0;
int maxGap = 0;
int stallCount = 0;
int burstCount = 0;
var sw = Stopwatch.StartNew();
Thread monitor = new Thread(() => {
while (sw.ElapsedMilliseconds < 60000) {
CSBI cur;
GetConsoleScreenBufferInfo(hOut, out cur);
int curOff = CursorLinear(cur);
long ts = sw.ElapsedMilliseconds;
if (curOff != prevOffset) {
int delta = curOff - prevOffset;
if (delta < 0) delta = 0; if (firstChangeMs == 0) firstChangeMs = ts;
int gap = 0;
if (lastChangeMs > 0) {
gap = (int)(ts - lastChangeMs);
if (gap > maxGap) maxGap = gap;
if (gap > 150) stallCount++;
if (delta > 8) burstCount++;
}
timestamps.Add(ts);
positions.Add(curOff);
deltas.Add(delta);
gaps.Add(gap);
lastChangeMs = ts;
prevOffset = curOff;
}
if (injDone && lastChangeMs > 0 && (ts - lastChangeMs) > 2000) break;
if (ts > 30000 && firstChangeMs == 0) break;
Thread.Sleep(2);
}
});
monitor.IsBackground = true;
monitor.Start();
Thread.Sleep(20);
long injStart = sw.ElapsedMilliseconds;
int charsSent = 0;
foreach (char c in text) {
InjectChar(hIn, c);
charsSent++;
Interlocked.Exchange(ref injectedCount, charsSent);
if (c == ' ') {
if (interMs > 0) Thread.Sleep(interMs);
} else {
if (intraMs > 0) Thread.Sleep(intraMs);
}
}
long injEnd = sw.ElapsedMilliseconds;
injDone = true;
monitor.Join(5000);
CloseHandle(hIn);
CloseHandle(hOut);
FreeConsole();
var sortedGaps = new List<int>();
foreach (int g in gaps) { if (g > 0) sortedGaps.Add(g); }
sortedGaps.Sort();
int n = sortedGaps.Count;
int p50 = n > 0 ? sortedGaps[n / 2] : 0;
int p90 = n > 0 ? sortedGaps[(int)(n * 0.9)] : 0;
int p95 = n > 0 ? sortedGaps[Math.Min((int)(n * 0.95), n - 1)] : 0;
int p99 = n > 0 ? sortedGaps[Math.Min((int)(n * 0.99), n - 1)] : 0;
long avgGap = 0;
if (n > 0) {
long sum = 0;
foreach (int g in sortedGaps) sum += g;
avgGap = sum / n;
}
long renderSpan = (lastChangeMs > 0 && firstChangeMs > 0) ? lastChangeMs - firstChangeMs : 0;
int totalRendered = prevOffset - baseOffset;
if (totalRendered < 0) totalRendered = 0;
Console.WriteLine("TS_MS,CURSOR_POS,DELTA,GAP_MS");
for (int i = 0; i < timestamps.Count; i++) {
Console.WriteLine("{0},{1},{2},{3}", timestamps[i], positions[i], deltas[i], gaps[i]);
}
Console.WriteLine("SUMMARY chars={0} inject_ms={1} render_ms={2} samples={3} rendered={4} stalls={5} bursts={6} max_gap={7} avg_gap={8} p50={9} p90={10} p95={11} p99={12}",
text.Length, injEnd - injStart, renderSpan, timestamps.Count, totalRendered,
stallCount, burstCount, maxGap, avgGap, p50, p90, p95, p99);
}
}