แนะนำ Pulsora: ฐานข้อมูลไทม์ซีรีส์ที่เขียนด้วย Rust สำหรับข้อมูลเทรด

เรามีกระแสข้อมูลพุ่งเข้ามาแต่ไม่มีที่ดี ๆ ให้เก็บมัน

ปัญหาภายในนั้นเฉพาะเจาะจง: จัดเก็บและคิวรีกระแสข้อมูลตลาดที่ใหญ่และต่อเนื่อง — tick, quote, bar ของสัญลักษณ์จำนวนมาก — ให้เร็วพอที่การนำเข้าจะไม่กลายเป็นคอขวด และคิวรีตามช่วงเวลาคืนค่ากลับมาโดยที่เราไม่ต้องกลั้นหายใจ ข้อมูลตลาดคือเวอร์ชันที่สะอาดและโหดที่สุดของภาระงานไทม์ซีรีส์ มันมาตามลำดับ มันมาตลอดเวลา และมันไม่เคยหยุด ปริมาณไม่ได้น่าสนใจเพราะมันใหญ่ครั้งเดียว แต่มันน่าสนใจเพราะมันใหญ่ทุก ๆ วินาที ตลอดไป

ตอนแรกเราลองสิ่งที่ชัดเจนก่อน: หยิบฐานข้อมูลไทม์ซีรีส์ที่มีอยู่แล้วมาใช้ ปกติแล้วนั่นคือการตัดสินใจที่ถูกต้อง และช่วงหนึ่งมันก็ใช้ได้ดี จากนั้นความต้องการด้านอัตราการเขียนก็ค่อย ๆ สูงขึ้น พื้นที่บนดิสก์โตเร็วยิ่งกว่า และคิวรีตามช่วงเวลาที่เราใช้งานจริงเริ่มรู้สึกถึงน้ำหนักของดัชนี ถึงจุดหนึ่ง ชั้นสตอเรจทำงานบัญชีต่อแถวมากกว่างานจริงบนแถวเสียอีก

เราจึงสร้าง Pulsora — ฐานข้อมูลไทม์ซีรีส์ที่เขียนด้วย Rust ปรับให้เหมาะกับข้อมูลตลาดและชุดข้อมูลที่เรียงตามเวลา มันเป็นโอเพนซอร์สบน GitHub ภายใต้สัญญาอนุญาต Apache-2.0 โพสต์นี้คือการแนะนำว่ามันคืออะไร ทำไมมันถึงมีอยู่ และจริง ๆ แล้วมันจัดเก็บและคิวรีข้อมูลอย่างไร — อ้างอิงจากโค้ด ไม่ใช่จากโบรชัวร์


ทำไมต้องสร้างเองแทนที่จะรับมาใช้

การสร้างฐานข้อมูลไม่ใช่สิ่งที่ทำกันเล่น ๆ เกณฑ์ที่ต้องผ่านให้ได้คือ: เราต้องการอะไรเป็นพิเศษที่เราไม่ได้รับมาในราคาที่ถูกพอ?

มีสามสิ่งที่ผุดขึ้นมาซ้ำ ๆ

อัตราการเขียนที่ขยายตามบล็อก ไม่ใช่ตามแถว กระแส tick คือแถวเล็ก ๆ ที่แทบเหมือนกันนับล้านแถว สตอเรจทั่วไปส่วนใหญ่อยากทำดัชนีให้ทุกแถว นั่นคือรายการดัชนีหนึ่งรายการต่อหนึ่งแถว และที่จำนวนแถวของเรา ดัชนีกลายเป็นต้นทุนหลัก — ทั้งในการขยายการเขียน บนดิสก์ และในการ compaction เราต้องการให้ต้นทุนสตอเรจต่อแถวเข้าใกล้ศูนย์ และให้ดัชนีขยายตามจำนวน บล็อก ที่เราเขียน ไม่ใช่ตามจำนวนแถว

การบีบอัดที่เข้าใจข้อมูล ข้อมูลตลาดบีบอัดได้สวยงามหากคุณมองมันเป็นคอลัมน์ของชนิดข้อมูลที่รู้แน่ชัด timestamp มาที่ช่วงเวลาเกือบสม่ำเสมอ ราคาเปลี่ยนช้า ๆ ปริมาณเป็นจำนวนเต็มเล็ก ๆ การบีบอัดแบบเรียงตามแถวทั่วไปทิ้งสิ่งเหล่านั้นไปเกือบหมด เราต้องการการบีบอัดแบบคอลัมน์ที่รู้ชนิดข้อมูล — แบบที่เปเปอร์ Gorilla ของ Facebook วางไว้ — สร้างมาในตัว ไม่ใช่แปะเพิ่มทีหลัง

อินเทอร์เฟซที่น่าเบื่อและยืดหยุ่นเรื่องฟอร์แมต เรานำเข้าจากผู้ผลิตข้อมูลหลายแหล่งและบริโภคเข้าสู่เครื่องมือหลายตัว เราไม่ต้องการโปรโตคอลสายเฉพาะตัว เราต้องการ POST CSV จากสคริปต์ สตรีม Apache Arrow จากไปป์ไลน์ และอ่านกลับเป็น JSON, Arrow หรือ CSV ขึ้นอยู่กับว่าใครเป็นคนถาม HTTP และฟอร์แมตคอลัมน์ที่รู้จักกันดี ไม่มีอะไรแปลกประหลาด

ไม่มีข้อใดที่แปลกใหม่ในตัวมันเอง แต่การผสมผสาน — สำหรับภาระงาน ของเรา ด้วยข้อจำกัดด้านพื้นที่ ของเรา — เพียงพอที่จะให้เหตุผลในการเขียนมันขึ้นมา และเมื่อคุณกำลังสร้างมันอยู่แล้ว การเขียนด้วย Rust เพื่อให้ได้ความถูกต้องตอน compile บนเส้นทางร้อนคือส่วนที่ง่าย เราเคยเขียนไว้ก่อนหน้านี้ว่าทำไมเราถึงสร้างเครื่องมือของเราเอง Pulsora อยู่ในสายเลือดเดียวกันนั้นพอดี


วันนี้ Pulsora คืออะไร

ขอพูดตรง ๆ เรื่องขอบเขต: Pulsora คือ 0.1.0 มันคือเอนจินที่ทำงานของมันได้ดี ไม่ใช่แพลตฟอร์มแบบคลัสเตอร์ มี replication รองรับหลายผู้เช่าพร้อม query planner มันคือสตอเรจไทม์ซีรีส์แบบคอลัมน์โหนดเดียว ฝัง RocksDB พร้อม REST API เท่านั้นเอง และเท่านั้นเองคือประเด็น

นี่คือรูปร่างของมัน:

  • แบ็กเอนด์สตอเรจ: RocksDB พร้อมชั้นคอลัมน์ที่เขียนเองทับไว้ด้านบน
  • ฟอร์แมตการนำเข้า: CSV, Apache Arrow IPC stream และ Protocol Buffers
  • ฟอร์แมตผลลัพธ์ของคิวรี: JSON (ค่าเริ่มต้น), Arrow IPC stream, Protobuf และ CSV — เจรจาผ่านเฮดเดอร์ Accept
  • สคีมา: อนุมานอัตโนมัติตอนนำเข้าครั้งแรก คุณไม่ต้องประกาศตาราง คุณ POST ข้อมูลแล้ว Pulsora หาคอลัมน์และชนิดข้อมูลเอง
  • อินเทอร์เฟซ: HTTP/REST API ที่ให้บริการโดย Axum บน Tokio
  • ความทนทาน: write-ahead log แบบเลือกเปิดได้ เพื่อให้แถวที่อยู่ในบัฟเฟอร์รอดจากการแครช

ทั้งหมดคือไบนารีเดียวชื่อ pulsora มีแฟล็กบรรทัดคำสั่งเดียว

# รันด้วยค่าเริ่มต้น
cargo run

# หรือชี้ไปยังไฟล์คอนฟิก
cargo run -- -c pulsora.toml

แฟล็ก -c/--config นั้นคือพื้นผิวบรรทัดคำสั่งทั้งหมด ที่เหลือทั้งหมดอยู่ใน TOML นี่เป็นความตั้งใจ — คอนฟิกคือ API สำหรับการดำเนินงาน ส่วน API ตอนรันไทม์คือ HTTP


มันจัดเก็บข้อมูลบนดิสก์อย่างไร

นี่คือส่วนที่เราใส่ใจมากที่สุด จึงเป็นส่วนที่ควรค่าแก่การอธิบาย

บล็อกแบบคอลัมน์ ไม่ใช่แถว

แถวเข้ามา แต่มันไม่ได้ถูกจัดเก็บเป็นแถว Pulsora สะสมแถวที่เข้ามาในบัฟเฟอร์ในหน่วยความจำ และเมื่อบัฟเฟอร์ถึงเกณฑ์ขนาด (buffer_size ค่าเริ่มต้น 1000) หรือเกณฑ์เวลา (flush_interval_ms) มันจะจัดกลุ่มแถวเหล่านั้นเป็น ColumnBlock — ก้อนของแถวที่จัดเก็บแบบคอลัมน์ต่อคอลัมน์ แต่ละคอลัมน์ถูกบีบอัดอย่างอิสระด้วยอัลกอริทึมที่เลือกตามชนิดข้อมูลของมัน

บล็อกคือหน่วยของการจัดเก็บ ColumnBlock มีหน้าตาคร่าว ๆ ดังนี้:

pub struct ColumnBlock {
    pub row_count: usize,                        // จำนวนแถวในบล็อกนี้
    pub columns: HashMap<String, Vec<u8>>,       // ข้อมูลคอลัมน์ที่บีบอัดแล้ว
    pub null_bitmaps: HashMap<String, Vec<u8>>,  // การติดตามค่า null ต่อคอลัมน์
}

การจัดเก็บแบบคอลัมน์แทนที่จะเป็นแถวคือสิ่งที่ทำให้การบีบอัดได้ผล เพราะทุกค่าในคอลัมน์มีชนิดข้อมูลเดียวกันและขนาดใกล้เคียงกัน นั่นคือเหตุผลทั้งหมดที่สตอเรจแบบคอลัมน์มีอยู่ และข้อมูลไทม์ซีรีส์คือภาระงานที่มันถูกคิดค้นขึ้นมาเพื่อรองรับ

การบีบอัดเฉพาะตามชนิดข้อมูล

คอลัมน์แต่ละชนิดได้รับกลยุทธ์การบีบอัดที่สร้างมาเพื่อรูปร่างของมัน ตามเอกสารของ repository:

ชนิดข้อมูล การบีบอัด อัตราส่วนทั่วไป (ตัวเลขของ repo) ทำไมจึงได้ผล
Timestamp Delta-of-delta + varint 5–10x ช่วงเวลาเกือบสม่ำเสมอ
Float XOR (Gorilla) + varfloat 2–5x ค่าที่เปลี่ยนแปลงช้า
Integer Delta + varint 3–8x ข้อมูลเรียงลำดับ / ตัวนับ
String การเข้ารหัสแบบพจนานุกรม 2–4x ค่าที่ซ้ำ ๆ (สัญลักษณ์)
Boolean การเข้ารหัสแบบ run-length (RLE) 10–50x ข้อมูลกระจัดกระจาย

เส้นทางของ timestamp คือกรณีตำรา tick มาที่ช่วงเวลาที่ เกือบ คงที่ ดังนั้นผลต่างอันดับสอง (delta-of-delta) มักเป็นศูนย์หรือเล็กจิ๋ว และ varint เข้ารหัสตัวเลขจิ๋ว ๆ ในหนึ่งไบต์ Float ใช้การแพ็กบิตแบบ XOR-กับค่าก่อนหน้า จากอัลกอริทึม Gorilla — เมื่อราคาแทบไม่ขยับ บิตที่ผ่าน XOR ส่วนใหญ่เป็นศูนย์และถูกแพ็กจนแทบหายไป สตริงอย่างสัญลักษณ์ ticker ซ้ำไม่รู้จบ ดังนั้นพจนานุกรมจึงแมปแต่ละสัญลักษณ์ที่ต่างกันไปยังจำนวนเต็มเล็ก ๆ เพียงครั้งเดียว

ใต้ทั้งหมดนั้น RocksDB ใส่การบีบอัดบล็อกทั่วไปอีกชั้นหนึ่ง — lz4 เป็นค่าเริ่มต้น โดยมี none, snappy และ zstd ให้เลือก ดังนั้นคุณจึงได้การเข้ารหัสที่รู้ชนิดข้อมูลก่อน แล้วตามด้วยการบีบอัดทั่วไปทับด้านบน

อัตราส่วนเหล่านี้คือคำกล่าวอ้างเรื่องเบนช์มาร์กของ repository เอง วัดด้วยชุด Criterion ภายใต้ benches/ ผมรายงานมันในฐานะตัวเลขของโปรเจกต์ ไม่ใช่คำตัดสินอิสระ — ลองรัน cargo bench กับข้อมูลของคุณเองและเชื่อตัวเลขนั้นแทน

ดัชนีที่ไม่โตตามจำนวนแถว

นี่คือการตัดสินใจด้านดีไซน์ที่ทุกอย่างหมุนรอบมัน

สตอเรจไทม์ซีรีส์ส่วนใหญ่เก็บดัชนีต่อแถวเพื่อให้หาแถวเดี่ยวใด ๆ ตามเวลาได้ Pulsora ไม่ทำ มันเก็บ ดัชนีระดับบล็อกเท่านั้น — เมตาดาตาเกี่ยวกับแต่ละบล็อก ไม่เคยเกี่ยวกับแถวรายตัว เลย์เอาต์คีย์บนดิสก์ ตามเอกสารสถาปัตยกรรม มีหน้าตาดังนี้:

Block index: [table_hash:u32]['B'][min_ts:i64][block_id:u64]
             value: [block_id][min_ts][max_ts][rows][min_id][max_id]   — หนึ่งรายการต่อบล็อก
Block data:  [table_hash:u32]['D'][block_id:u64]                       — บล็อกแบบคอลัมน์ที่บีบอัดแล้ว
Overrides:   [table_hash:u32]['O'][block_id:u64]                       — ตำแหน่งของแถวที่ตายแล้ว

table_hash คือแฮช FNV-1a ขนาด 4 ไบต์ของชื่อตาราง ใช้เป็นคีย์พรีฟิกซ์เพื่อให้ตารางถูกแยกกันโดยไม่ต้องลากชื่อสตริงไปกับทุกคีย์ หลังพรีฟิกซ์ ไบต์เดียว ('B', 'D', 'O') แยกดัชนี ข้อมูล และชุด override ออกจากกัน

ผลที่ตามมา: ต้นทุนสตอเรจขยายตามจำนวนบล็อก ไม่ใช่จำนวนแถว เขียนหนึ่งพันล้านแถวแล้วคุณไม่ได้เพิ่มรายการดัชนีใด ๆ นอกเหนือจากบล็อกที่ใส่มันไว้ นั่นคือข้อกำหนดที่เราซื้อในราคาถูกไม่ได้ และคือแก่นของเหตุผลที่ Pulsora มีอยู่

การอัปเดตแถวจัดการได้โดยไม่ต้องเขียนบล็อกใหม่ REPLACE จะทำเครื่องหมายตำแหน่งของสำเนาเดิมที่ถูกแทนที่ใน ชุด override ของบล็อกเก่า — ชุดของตำแหน่งแถวที่ตายแล้วต่อบล็อก ถูกผสานผ่านตัวดำเนินการ merge ของ RocksDB (set union) ใน write batch เดียวกันกับข้อมูลใหม่ การตรวจว่าแถวยังมีชีวิตคือการอ่านแบบจุดหนึ่งครั้งต่อบล็อก ไม่ใช่การ lookup ต่อแถว


เส้นทางการเขียน

การนำเข้าคือ POST สคีมาถูกอนุมานในครั้งแรก

curl -X POST http://localhost:8080/tables/stocks/ingest \
  -H "Content-Type: text/csv" \
  --data-binary @ticks.csv

แถวเฮดเดอร์ของ CSV นั้นกลายเป็นสคีมา Pulsora สุ่มตัวอย่างค่า ตรวจหาชนิดของแต่ละคอลัมน์ (Integer, Float, String, Boolean, Timestamp) และระบุคอลัมน์ timestamp อัตโนมัติ — มันเข้าใจ RFC 3339, YYYY-MM-DD HH:MM:SS, แบบวันที่อย่างเดียว และ Unix timestamp ทั้งแบบวินาทีและมิลลิวินาที จากนั้นเป็นต้นมา สคีมาของตารางนั้นถูกตรึงไว้และข้อมูลที่เข้ามาจะถูกตรวจสอบกับมัน

สำหรับไปป์ไลน์ที่ต้องการอัตราสูงกว่า ข้ามการแพร์ส CSV ไปเลยแล้วสตรีม Apache Arrow:

curl -X POST http://localhost:8080/tables/stocks/ingest \
  -H "Content-Type: application/vnd.apache.arrow.stream" \
  --data-binary @ticks.arrow

Arrow มาพร้อมสคีมาที่แนบมาแล้ว จึงไม่มีขั้นตอนอนุมานและไม่มีการแพร์สข้อความ — คอลัมน์เป็นคอลัมน์อยู่แล้ว การนำเข้า Protobuf ทำงานแบบเดียวกันสำหรับผู้ผลิตข้อมูลที่พูดมันได้อยู่แล้ว

ภายในนั้น ขั้นตอนคือ: แพร์สอินพุต ตรวจสอบหรืออนุมานสคีมา บัฟเฟอร์แถวในหน่วยความจำ (ขจัดข้อมูลซ้ำตอนเข้ามา การเขียนล่าสุดชนะ) และ — หากเปิด WAL — ต่อท้ายแต่ละแถวลงในไฟล์ .wal ก่อนที่มันจะแตะหน่วยความจำ เพื่อให้การแครชไม่ทำให้ข้อมูลในบัฟเฟอร์หาย เมื่อบัฟเฟอร์ flush แถวจะกลายเป็น ColumnBlock ที่บีบอัด บล็อกและรายการดัชนีของมันถูกเขียนลง RocksDB ใน batch เดียว และ WAL ถูกตัดทิ้ง (truncate)


เส้นทางการอ่าน

คิวรีคือ GET พร้อมช่วงเวลาและการแบ่งหน้า

# ทั้งหมด (ถูกจำกัดด้วย limit ค่าเริ่มต้น)
curl "http://localhost:8080/tables/stocks/query"

# ช่วงเวลาหนึ่ง
curl "http://localhost:8080/tables/stocks/query?start=2024-01-01T09:30:00&end=2024-01-01T16:00:00"

# แบ่งหน้า
curl "http://localhost:8080/tables/stocks/query?limit=1000&offset=0"

start และ end รับฟอร์แมต timestamp ชุดที่ยืดหยุ่นชุดเดียวกับการนำเข้า limit ค่าเริ่มต้นคือ 1000 โดยไม่มีการบังคับค่าสูงสุด ส่วน offset ข้ามแถวเพื่อการแบ่งหน้า

เอนจินคิวรีแก้คำขอด้วยดัชนีบล็อกก่อน: มันสร้างช่วงคีย์ไบนารีจากขอบเขตเวลาและวนเฉพาะบล็อกที่ [min_ts, max_ts] ทับซ้อนกับช่วงนั้น มันรวบรวมบล็อกผู้สมัคร ดึงแต่ละบล็อกที่ไม่ซ้ำมาครั้งเดียว คลายการบีบอัด แคชผลลัพธ์ที่คลายแล้วเพื่อให้การเข้าถึงซ้ำภายในคิวรีไม่ต้องคลายใหม่ ใช้ชุด override เพื่อข้ามแถวที่ตายแล้ว ดึงแถวที่ต้องการ และซีเรียลไลซ์มันในฟอร์แมตที่เฮดเดอร์ Accept ขอ

อยากได้ผลลัพธ์กลับมาเป็น Arrow สำหรับไปป์ไลน์ปลายทางแทน JSON? เปลี่ยนเฮดเดอร์เดียว:

curl "http://localhost:8080/tables/stocks/query?limit=10000" \
  -H "Accept: application/vnd.apache.arrow.stream" > out.arrow

curl "http://localhost:8080/tables/stocks/query?limit=10000" \
  -H "Accept: text/csv" > out.csv

คิวรีเดียวกัน สี่ฟอร์แมตผลลัพธ์ที่เป็นไปได้ ไม่มีขั้นตอนแปลงฝั่งคุณ ยังมีเอนด์พอยต์อ่านอีกหยิบมือหนึ่ง — GET /tables เพื่อแสดงรายการตาราง, GET /tables/{name}/schema สำหรับสคีมาที่อนุมาน, GET /tables/{name}/count สำหรับนับจำนวนแถว, GET /tables/{name}/row/{id} เพื่อดึงแถวเดียวด้วย id และ GET /health


ลองใช้

Pulsora build ได้ด้วย Rust toolchain ปัจจุบันและรันเป็นไบนารีเดียว

# โคลนและรันจากซอร์ส
git clone https://github.com/muvon/pulsora.git
cd pulsora
cargo run

# หรือติดตั้งไบนารีโดยตรง
cargo install --git https://github.com/muvon/pulsora.git

# นำเข้า CSV
curl -X POST http://localhost:8080/tables/stocks/ingest \
  -H "Content-Type: text/csv" \
  -d "timestamp,symbol,price,volume
2024-01-01 09:30:00,AAPL,150.00,1000
2024-01-01 09:31:00,AAPL,150.25,1500
2024-01-01 09:32:00,AAPL,149.75,2000"

# อ่านกลับ
curl "http://localhost:8080/tables/stocks/query?limit=10"

# ดูสคีมาที่อนุมาน
curl "http://localhost:8080/tables/stocks/schema"

การปรับจูนอยู่ใน pulsora.toml การตั้งค่าที่ควรรู้ก่อน:

[storage]
data_dir = "./data"
buffer_size = 1000          # แถวที่บัฟเฟอร์ก่อนเขียนบล็อก
flush_interval_ms = 1000    # เวลาสูงสุดที่แถวรอในบัฟเฟอร์
wal_enabled = true          # แถวในบัฟเฟอร์รอดจากการแครช

[ingestion]
batch_size = 10000          # แถวต่อ batch การเขียนลงฐานข้อมูล

[performance]
compression = "lz4"         # none | snappy | lz4 | zstd
cache_size_mb = 256         # แคชบล็อกสำหรับการอ่าน

หากคุณให้ความสำคัญกับการบีบอัดสูงสุดมากกว่าความหน่วง ตั้ง flush_interval_ms = 0 เพื่อโหมด batch-only ที่กั้นแถวไว้จนกว่าบัฟเฟอร์จะเต็ม ทำให้ได้บล็อกที่ใหญ่กว่าและบีบอัดดีกว่า หากคุณให้ความสำคัญกับความหน่วงในการอ่านข้อมูลร้อน เพิ่ม cache_size_mb ชุดปุ่มปรับทั้งหมดมีบันทึกไว้ใน doc/CONFIGURATION.md


มันไม่ใช่อะไร และต่อไปคืออะไร

ผมจะช่วยคุณให้ไม่ต้องผิดหวังจากการค้นพบสิ่งเหล่านี้ด้วยวิธีที่ยากลำบาก วันนี้ Pulsora เป็นโหนดเดียว ไม่มีคลัสเตอร์ ไม่มี replication ไม่มี SQL ไม่มี query planner ทั่วไป ไม่มี predicate pushdown นอกเหนือจากช่วงเวลา ไม่มีการจำกัดอัตรา คิวรีกรองตามเวลาและแบ่งหน้า มันไม่ทำ GROUP BY ไฟล์ doc/ARCHITECTURE.md ระบุทิศทางที่เรากำลังมองอยู่ — column pruning และ predicate pushdown, แคชบล็อกที่หนุนหลังด้วยดิสก์, สตอเรจแบบแบ่งชั้นร้อน/อุ่น/เย็น และการทำคลัสเตอร์ — แต่นั่นคือโรดแมป ไม่ใช่ความจริง ผมอยากให้คุณรู้ขอบเขตมากกว่าให้คุณไปชนมัน

สิ่งที่มัน เป็น ในวันนี้คือเอนจินที่โฟกัส ซึ่งนำเข้ากระแสข้อมูลที่เรียงตามเวลา บีบอัดมันอย่างหนักด้วยอัลกอริทึมที่เข้าใจมัน และให้บริการคิวรีตามช่วงเวลาโดยไม่มีดัชนีที่พองตามจำนวนแถว นั่นคือปัญหาที่เรามี และเป็นปัญหาที่ Pulsora แก้


โอเพนซอร์ส

Pulsora อยู่บน GitHub ภายใต้ Apache-2.0 มันคือ Rust — RocksDB สำหรับสตอเรจ, Axum และ Tokio สำหรับ API, Arrow และ Prost สำหรับฟอร์แมตคอลัมน์และ protobuf — และ build ได้ด้วย cargo build ธรรมดา ชุดเบนช์มาร์กอยู่ใน benches/ หากคุณอยากวัดประสิทธิภาพการนำเข้าและคิวรีกับข้อมูลของคุณเองแทนที่จะเชื่อของเรา

หากคุณกำลังจมอยู่ในแถวที่เรียงตามเวลา — tick ตลาด ค่าจากเซ็นเซอร์ อะไรก็ตามที่มาตามลำดับและไม่เคยหยุด — โคลนมัน ชี้สตรีมไปที่มัน แล้วบอกเราว่ามันพังตรงไหน นั่นคือทางที่เร็วที่สุดที่มันจะดีขึ้น

— Don

Pulsora เป็นโอเพนซอร์สบน GitHub ที่ github.com/muvon/pulsora ภายใต้สัญญาอนุญาต Apache-2.0 เจอบั๊กหรืออยากได้ฟีเจอร์? เปิด issue