Xterm.js: Peningkatan kinerja penyangga

Dibuat pada 13 Jul 2017  ·  73Komentar  ·  Sumber: xtermjs/xterm.js

Masalah

Penyimpanan

Saat ini buffer kami menggunakan terlalu banyak memori, terutama untuk aplikasi yang meluncurkan banyak terminal dengan set scrollback besar. Misalnya, demo menggunakan terminal 160x24 dengan 5000 scrollback diisi membutuhkan sekitar 34mb memori (lihat https://github.com/Microsoft/vscode/issues/29840#issuecomment-314539964), ingat itu hanya satu terminal dan monitor 1080p akan kemungkinan menggunakan terminal yang lebih luas. Juga, untuk mendukung truecolor (https://github.com/sourcelair/xterm.js/issues/484), setiap karakter perlu menyimpan 2 jenis number yang hampir menggandakan konsumsi memori saat ini dari penyangga.

Pengambilan teks baris yang lambat

Ada masalah lain karena perlu mengambil teks baris yang sebenarnya dengan cepat. Alasan mengapa ini lambat adalah karena cara data ditata; baris berisi array karakter, masing-masing memiliki string karakter tunggal. Jadi kita akan membangun string dan kemudian akan segera diambil untuk pengumpulan sampah. Sebelumnya kita tidak perlu melakukan ini sama sekali karena teks ditarik dari buffer baris (secara berurutan) dan dirender ke DOM. Namun, ini menjadi hal yang semakin berguna untuk dilakukan saat kami meningkatkan xterm.js lebih lanjut, fitur seperti pilihan dan tautan menarik data ini. Sekali lagi menggunakan contoh scrollback 160x24/5000, dibutuhkan 30-60ms untuk menyalin seluruh buffer pada Macbook Pro Pertengahan 2014.

Mendukung masa depan

Masalah potensial lainnya di masa depan adalah ketika kita melihat memperkenalkan beberapa model tampilan yang mungkin perlu menduplikasi beberapa atau semua data dalam buffer, hal semacam ini akan diperlukan untuk mengimplementasikan reflow (https://github.com/sourcelair /xterm.js/issues/622) dengan benar (https://github.com/sourcelair/xterm.js/pull/644#issuecomment-298058556) dan mungkin juga diperlukan untuk mendukung pembaca layar dengan benar (https://github.com /sourcelair/xterm.js/issues/731). Tentu akan bagus untuk memiliki ruang gerak dalam hal memori.

Diskusi ini dimulai di https://github.com/sourcelair/xterm.js/issues/484 , ini membahas lebih detail dan mengusulkan beberapa solusi tambahan.

Saya condong ke solusi 3 dan bergerak menuju solusi 5 jika ada waktu dan itu menunjukkan peningkatan yang nyata. Akan menyukai umpan balik! /cc @jerch , @mofux , @rauchg , @parisk

1. Solusi sederhana

Ini pada dasarnya apa yang kita lakukan sekarang, hanya dengan truecolor fg dan bg ditambahkan.

// [0]: charIndex
// [1]: width
// [2]: attributes
// [3]: truecolor bg
// [4]: truecolor fg
type CharData = [string, number, number, number, number];

type LineData = CharData[];

kelebihan

  • Sangat sederhana

Kontra

  • Terlalu banyak memori yang dikonsumsi, akan hampir menggandakan penggunaan memori kita saat ini yang sudah terlalu tinggi.

2. Tarik teks dari CharData

Ini akan menyimpan string terhadap garis daripada garis, ini mungkin akan melihat keuntungan yang sangat besar dalam seleksi dan menghubungkan dan akan lebih berguna seiring berjalannya waktu memiliki akses cepat ke seluruh string baris.

interface ILineData {
  // This would provide fast access to the entire line which is becoming more
  // and more important as time goes on (selection and links need to construct
  // this currently). This would need to reconstruct text whenever charData
  // changes though. We cannot lazily evaluate text due to the chars not being
  // stored in CharData
  text: string;
  charData: CharData[];
}

// [0]: charIndex
// [1]: attributes
// [2]: truecolor bg
// [3]: truecolor fg
type CharData = Int32Array;

kelebihan

  • Tidak perlu merekonstruksi garis kapan pun kita membutuhkannya.
  • Memori lebih rendah dari hari ini karena penggunaan Int32Array

Kontra

  • Lambat untuk memperbarui karakter individu, seluruh string perlu dibuat ulang untuk perubahan karakter tunggal.

3. Simpan atribut dalam rentang

Menarik atribut keluar dan mengaitkannya dengan rentang. Karena tidak akan pernah ada atribut yang tumpang tindih, ini dapat ditata secara berurutan.

type LineData = CharData[]

// [0]: The character
// [1]: The width
type CharData = [string, number];

class CharAttributes {
  public readonly _start: [number, number];
  public readonly _end: [number, number];
  private _data: Int32Array;

  // Getters pull data from _data (woo encapsulation!)
  public get flags(): number;
  public get truecolorBg(): number;
  public get truecolorFg(): number;
}

class Buffer extends CircularList<LineData> {
  // Sorted list since items are almost always pushed to end
  private _attributes: CharAttributes[];

  public getAttributesForRows(start: number, end: number): CharAttributes[] {
    // Binary search _attributes and return all visible CharAttributes to be
    // applied by the renderer
  }
}

kelebihan

  • Memori lebih rendah dari hari ini meskipun kami juga menyimpan data truecolor
  • Dapat mengoptimalkan penerapan atribut, daripada memeriksa atribut setiap karakter dan membedakannya dengan yang sebelumnya
  • Mengenkapsulasi kompleksitas penyimpanan data di dalam array ( .flags alih-alih [0] )

Kontra

  • Mengubah atribut berbagai karakter di dalam rentang lain lebih kompleks

4. Masukkan atribut ke dalam cache

Idenya di sini adalah untuk memanfaatkan fakta bahwa umumnya tidak ada banyak gaya dalam satu sesi terminal, jadi kita tidak boleh membuat sesedikit yang diperlukan dan menggunakannya kembali.

// [0]: charIndex
// [1]: width
type CharData = [string, number, CharAttributes];

type LineData = CharData[];

class CharAttributes {
  private _data: Int32Array;

  // Getters pull data from _data (woo encapsulation!)
  public get flags(): number;
  public get truecolorBg(): number;
  public get truecolorFg(): number;
}

interface ICharAttributeCache {
  // Never construct duplicate CharAttributes, figuring how the best way to
  // access both in the best and worst case is the tricky part here
  getAttributes(flags: number, fg: number, bg: number): CharAttributes;
}

kelebihan

  • Penggunaan memori serupa dengan hari ini meskipun kami juga menyimpan data truecolor
  • Mengenkapsulasi kompleksitas penyimpanan data di dalam array ( .flags alih-alih [0] )

Kontra

  • Penghematan memori lebih sedikit daripada pendekatan rentang

5. Hibrida 3 & 4

type LineData = CharData[]

// [0]: The character
// [1]: The width
type CharData = [string, number];

class CharAttributes {
  private _data: Int32Array;

  // Getters pull data from _data (woo encapsulation!)
  public get flags(): number;
  public get truecolorBg(): number;
  public get truecolorFg(): number;
}

interface CharAttributeEntry {
  attributes: CharAttributes,
  start: [number, number],
  end: [number, number]
}

class Buffer extends CircularList<LineData> {
  // Sorted list since items are almost always pushed to end
  private _attributes: CharAttributeEntry[];
  private _attributeCache: ICharAttributeCache;

  public getAttributesForRows(start: number, end: number): CharAttributeEntry[] {
    // Binary search _attributes and return all visible CharAttributeEntry's to
    // be applied by the renderer
  }
}

interface ICharAttributeCache {
  // Never construct duplicate CharAttributes, figuring how the best way to
  // access both in the best and worst case is the tricky part here
  getAttributes(flags: number, fg: number, bg: number): CharAttributes;
}

kelebihan

  • Secara potensial tercepat dan paling hemat memori
  • Sangat hemat memori ketika buffer berisi banyak blok dengan gaya tetapi hanya dari beberapa gaya (kasus umum)
  • Mengenkapsulasi kompleksitas penyimpanan data di dalam array ( .flags alih-alih [0] )

Kontra

  • Lebih kompleks daripada solusi lain, mungkin tidak layak menyertakan cache jika kita sudah menyimpan satu CharAttributes per blok?
  • Overhead ekstra di objek CharAttributeEntry
  • Mengubah atribut berbagai karakter di dalam rentang lain lebih kompleks

6. Hibrida 2 & 3

Ini mengambil solusi 3 tetapi juga menambahkan dengan malas mengevaluasi string teks untuk akses cepat ke teks baris. Karena kita juga menyimpan karakter di CharData kita bisa dengan malas mengevaluasinya.

type LineData = {
  text: string,
  CharData[]
}

// [0]: The character
// [1]: The width
type CharData = [string, number];

class CharAttributes {
  public readonly _start: [number, number];
  public readonly _end: [number, number];
  private _data: Int32Array;

  // Getters pull data from _data (woo encapsulation!)
  public get flags(): number;
  public get truecolorBg(): number;
  public get truecolorFg(): number;
}

class Buffer extends CircularList<LineData> {
  // Sorted list since items are almost always pushed to end
  private _attributes: CharAttributes[];

  public getAttributesForRows(start: number, end: number): CharAttributes[] {
    // Binary search _attributes and return all visible CharAttributes to be
    // applied by the renderer
  }

  // If we construct the line, hang onto it
  public getLineText(line: number): string;
}

kelebihan

  • Memori lebih rendah dari hari ini meskipun kami juga menyimpan data truecolor
  • Dapat mengoptimalkan penerapan atribut, daripada memeriksa atribut setiap karakter dan membedakannya dengan yang sebelumnya
  • Meringkas kompleksitas penyimpanan data di dalam array ( .flags alih-alih [0] )
  • Akses lebih cepat ke string baris yang sebenarnya

Kontra

  • Memori ekstra karena tergantung pada string baris
  • Mengubah atribut berbagai karakter di dalam rentang lain lebih kompleks

Solusi yang tidak akan berhasil

  • Menyimpan string sebagai int di dalam Int32Array tidak akan berfungsi karena perlu waktu lama untuk mengonversi int kembali menjadi karakter.
areperformance typplan typproposal

Komentar yang paling membantu

Kondisi saat ini:

Setelah:

Semua 73 komentar

Pendekatan lain yang dapat digabungkan: gunakan indexeddb, websql atau filesystem api untuk mengeluarkan entri scrollback yang tidak aktif ke disk

Usulan yang bagus. Saya setuju dengan itu 3. adalah cara terbaik untuk saat ini karena memungkinkan kita menghemat memori sambil mendukung warna asli juga.

Jika kita sampai di sana dan segala sesuatunya terus berjalan dengan baik, kita kemudian dapat mengoptimalkan seperti yang diusulkan di 5. atau dengan cara lain apa pun yang muncul di benak kita saat itu dan masuk akal.

3. bagus 👍.

@mofux , sementara pasti ada kasus penggunaan untuk menggunakan teknik yang didukung penyimpanan disk untuk mengurangi jejak memori, ini dapat menurunkan pengalaman pengguna perpustakaan di lingkungan browser yang meminta izin pengguna untuk menggunakan penyimpanan disk.

Tentang Mendukung masa depan :
Semakin saya memikirkannya, semakin banyak gagasan untuk memiliki WebWorker yang melakukan semua pekerjaan berat untuk mengurai data tty, mempertahankan buffer baris, mencocokkan tautan, mencocokkan token pencarian, dan semacamnya menarik bagi saya. Pada dasarnya melakukan pekerjaan berat di utas latar belakang yang terpisah tanpa memblokir ui. Tapi saya pikir ini harus menjadi bagian dari diskusi terpisah mungkin menuju rilis 4.0

+100 tentang WebWorker di masa mendatang, tetapi saya pikir kami perlu mengubah daftar versi browser yang kami dukung karena tidak semuanya dapat menggunakannya...

Ketika saya mengatakan Int32Array , ini akan menjadi array biasa jika tidak didukung oleh lingkungan.

@mofux pemikiran yang baik dengan WebWorker di masa depan 👍

@AndrienkoAleksandr ya jika kita ingin menggunakan WebWorker kita harus tetap mendukung alternatif juga melalui deteksi fitur.

Wah bagus daftarnya :)

Saya juga cenderung condong ke 3. karena menjanjikan pemotongan besar dalam konsumsi memori untuk lebih dari 90% dari penggunaan terminal biasa. Optimalisasi memori imho harus menjadi tujuan utama pada tahap ini. Pengoptimalan lebih lanjut untuk kasus penggunaan tertentu mungkin berlaku di atas ini (apa yang terlintas dalam pikiran saya: "kanvas seperti aplikasi" seperti ncurses dan semacamnya akan menggunakan banyak pembaruan sel tunggal dan agak menurunkan daftar [start, end] dari waktu ke waktu) .

@AndrienkoAleksandr ya saya suka ide webworker juga karena bisa mengangkat _some_ beban dari mainthread. Masalah di sini adalah (selain fakta bahwa itu mungkin tidak didukung oleh semua sistem target yang diinginkan) adalah _some_ - bagian JS bukan masalah besar lagi dengan semua pengoptimalan, xterm.js telah melihat dari waktu ke waktu. Masalah nyata dari segi kinerja adalah tata letak/rendering browser...

@mofux Hal paging ke beberapa "memori asing" adalah ide yang bagus, meskipun itu harus menjadi bagian dari beberapa abstraksi yang lebih tinggi dan bukan dari "berikan widget terminal interaktif" yang xterm.js. Ini dapat dicapai dengan addon imho.

Di luar topik: Melakukan beberapa tes dengan array vs. typedarrays vs. asm.js. Yang bisa saya katakan - OMG, ini seperti 1 : 1,5 : 10 untuk beban dan set variabel sederhana (pada FF bahkan lebih). Jika kecepatan JS murni benar-benar mulai sakit, "gunakan asm" mungkin ada untuk menyelamatkan. Tapi saya akan melihat ini sebagai upaya terakhir karena itu akan menyiratkan perubahan mendasar. Dan webassembly belum siap untuk dikirim.

Di luar topik: Melakukan beberapa tes dengan array vs. typedarrays vs. asm.js. Yang bisa saya katakan - OMG, ini seperti 1: 1,5: 10 untuk beban dan set variabel sederhana (pada FF bahkan lebih)

@jerch untuk memperjelas, apakah array vs typedarrays adalah 1:1 hingga 1:5?

Woops tangkapan yang bagus dengan koma - maksud saya 10:15:100 segi kecepatan. Tetapi hanya pada array yang diketik FF yang sedikit lebih cepat dari array normal. asm setidaknya 10 kali lebih cepat daripada array js di semua browser - diuji dengan FF, webkit (Safari), blink/V8 (Chrome, Opera).

@jerch keren, kecepatan 50% dari typedarrays selain memori yang lebih baik pasti layak untuk diinvestasikan untuk saat ini.

Ide untuk menghemat memori - mungkin kita bisa menghilangkan width untuk setiap karakter. Akan mencoba mengimplementasikan versi wcwidth yang lebih murah.

@jerch kita perlu mengaksesnya sedikit, dan kita tidak bisa malas memuatnya atau apa pun karena ketika reflow datang kita akan membutuhkan lebar setiap karakter di buffer. Bahkan jika itu cepat, kami mungkin masih ingin menyimpannya.

Mungkin lebih baik menjadikannya opsional, dengan asumsi 1 jika tidak ditentukan:

type CharData = [string, number?]; // not sure if this is valid syntax

[
  // 'a'
  ['a'],
  // '文'
  ['文', 2],
  // after wide
  ['', 0],
  ...
]

@Tyriar Ya - karena saya sudah menulisnya, silakan lihat di PR #798
Speedup adalah 10 hingga 15 kali di komputer saya dengan biaya 16k byte untuk tabel pencarian. Mungkin kombinasi keduanya dimungkinkan jika masih diperlukan.

Beberapa tanda lainnya yang akan kami dukung di masa mendatang: https://github.com/sourcelair/xterm.js/issues/580

Pikiran lain: Hanya bagian bawah terminal ( Terminal.ybase to Terminal.ybase + Terminal.rows ) yang dinamis. Scrollback yang membentuk sebagian besar data benar-benar statis, mungkin kita bisa memanfaatkan ini. Saya tidak tahu ini sampai baru-baru ini, tetapi bahkan hal-hal seperti menghapus baris (DL, CSI Ps M) tidak membawa scrollback kembali ke bawah melainkan menyisipkan baris lain. Demikian pula, gulir ke atas (SU, CSI Ps S) menghapus item di Terminal.scrollTop dan menyisipkan item di Terminal.scrollBottom .

Mengelola bagian dinamis bawah terminal secara mandiri dan mendorong untuk menggulir kembali ketika saluran didorong keluar dapat menghasilkan beberapa keuntungan yang signifikan. Misalnya, bagian bawah bisa lebih bertele-tele untuk mendukung modifikasi atribut, akses lebih cepat, dll. Sedangkan scrollback bisa lebih dari format arsip seperti yang diusulkan di atas.

Pemikiran lain: mungkin ide yang lebih baik untuk membatasi CharAttributeEntry ke baris karena begitulah cara kerja sebagian besar aplikasi. Juga jika terminal diubah ukurannya maka bantalan "kosong" ditambahkan ke kanan yang tidak memiliki gaya yang sama.

misalnya:

screen shot 2017-08-07 at 8 51 52 pm

Di sebelah kanan perbedaan merah/hijau adalah sel "kosong" tanpa gaya.

@Tyriar
Adakah kesempatan untuk memasukkan kembali masalah ini ke dalam agenda? Setidaknya untuk program intensif keluaran, cara berbeda untuk menyimpan data terminal dapat menghemat banyak memori dan waktu. Beberapa hibrida 2/3/4 akan memberikan peningkatan throughput yang besar, jika kita dapat menghindari pemisahan dan penyimpanan karakter tunggal dari string input. Juga menyimpan atribut hanya setelah diubah akan membantu menghemat memori.

Contoh:
Dengan parser baru, kita dapat menyimpan banyak karakter input tanpa mengotak-atik atribut, karena kita tahu bahwa mereka tidak akan berubah di tengah string itu. Atribut string itu dapat disimpan di beberapa struktur data atau atribut lain bersama dengan wcwidths (ya kita masih membutuhkannya untuk menemukannya) dan jeda baris dan berhenti. Ini pada dasarnya akan menyerahkan model sel saat data masuk.
Masalah muncul jika ada sesuatu yang masuk dan ingin memiliki representasi penelusuran data terminal (mis. penyaji atau urutan pelarian/pengguna ingin memindahkan kursor). Kita masih harus melakukan perhitungan sel jika itu terjadi, tetapi seharusnya cukup untuk melakukan ini hanya untuk konten di dalam kolom dan baris terminal. (Belum yakin tentang konten yang digulir, yang mungkin lebih dapat di-cache dan murah untuk digambar ulang.)

@jerch Saya akan bertemu @mofux suatu hari di Praha dalam beberapa minggu dan kami akan melakukan/memulai beberapa perbaikan internal tentang bagaimana atribut teks ditangani yang mencakup ini

Dari https://github.com/xtermjs/xterm.js/pull/1460#issuecomment -390500944

Algo agak mahal karena setiap karakter perlu dievaluasi dua kali

@jerch jika Anda memiliki ide tentang akses teks yang lebih cepat dari buffer, beri tahu kami. Saat ini sebagian besar hanya satu karakter seperti yang Anda tahu, tetapi bisa berupa ArrayBuffer , string, dll. Saya telah berpikir bahwa kita harus berpikir untuk mengambil lebih banyak keuntungan dari scrollback yang entah bagaimana tidak dapat diubah.

Yah saya telah banyak bereksperimen dengan ArrayBuffers di masa lalu:

  • mereka sedikit lebih buruk daripada Array mengenai runtime untuk metode tipikal (mungkin masih kurang dioptimalkan oleh vendor mesin)
  • new UintXXArray jauh lebih buruk daripada pembuatan array literal dengan []
  • mereka terbayar beberapa kali jika Anda dapat melakukan pra-alokasi dan menggunakan kembali struktur data (hingga 10 kali), di sinilah sifat daftar tertaut dari array campuran memakan kinerja karena alokasi yang berat dan gc di belakang layar
  • untuk data string, konversi maju dan mundur memakan semua manfaat - sayang sekali JS tidak menyediakan string asli ke konverter Uint16Array (sebagian dapat dilakukan dengan TextEncoder meskipun)

Temuan saya tentang ArrayBuffer menyarankan untuk tidak menggunakannya untuk data string karena penalti konversi. Secara teori terminal dapat menggunakan ArrayBuffer dari node-pty hingga data terminal (ini akan menghemat beberapa konversi dalam perjalanan ke frontend), tidak yakin apakah rendering dapat dilakukan dengan cara itu, saya pikir untuk membuat hal itu selalu membutuhkan konversi uint16_t menjadi string final. Tetapi bahkan satu pembuatan string terakhir akan memakan sebagian besar runtime yang disimpan - dan lebih jauh lagi, akan mengubah terminal secara internal menjadi binatang C-ish yang jelek. Oleh karena itu saya menyerah pendekatan ini.

TL;DR ArrayBuffer lebih unggul jika Anda dapat mengalokasikan dan menggunakan kembali struktur data. Untuk yang lainnya, array normal lebih baik. String tidak layak untuk dimasukkan ke dalam ArrayBuffers.

Ide baru yang saya buat mencoba untuk menurunkan pembuatan string sebanyak mungkin, khususnya. mencoba untuk menghindari perpecahan jahat dan bergabung. Ini agak didasarkan pada ide ke-2 Anda di atas dengan metode InputHandler.print , wcwidth dan perhentian baris:

  • print sekarang mendapatkan seluruh string hingga beberapa baris terminal
  • simpan string tersebut dalam daftar penunjuk sederhana tanpa perubahan apa pun (tanpa alokasi string atau gc, alokasi daftar dapat dihindari jika digunakan dengan struktur yang telah dialokasikan sebelumnya) bersama dengan atribut saat ini
  • memajukan kursor dengan wcwidth(string) % cols
  • kasus khusus \n (penghentian garis keras): memajukan kursor dengan satu baris, menandai posisi dalam daftar penunjuk sebagai pemutusan paksa
  • limpahan baris kasus khusus dengan wrapAround: tandai posisi dalam string sebagai jeda baris lunak
  • kasus khusus \r : memuat konten baris terakhir (dari posisi kursor saat ini ke jeda baris terakhir) ke dalam beberapa buffer baris untuk ditimpa
  • data mengalir seperti di atas, meskipun kasus \r tidak diperlukan abstraksi sel atau pemisahan string
  • perubahan atribut tidak masalah, selama tidak ada yang meminta representasi cols x rows (mereka hanya mengubah flag attr yang disimpan bersama dengan seluruh string)

Btw wcwidths adalah bagian dari algo grapheme, jadi ini mungkin dapat dipertukarkan di masa depan.

Sekarang bagian berbahaya 1 - seseorang ingin memindahkan kursor di dalam cols x rows :

  • pindahkan cols mundur dalam jeda baris - awal dari konten terminal saat ini
  • setiap jeda baris menunjukkan garis terminal nyata
  • memuat barang ke dalam model sel hanya untuk satu halaman (belum yakin apakah ini juga dapat dihilangkan dengan pemosisian string yang pintar)
  • lakukan pekerjaan yang buruk: jika perubahan atribut diminta, kami agak kurang beruntung dan harus kembali ke model sel penuh atau model string split & insert (yang terakhir mungkin memperkenalkan kinerja buruk)
  • data mengalir lagi, sekarang dengan data string & attrs yang terdegradasi di buffer untuk halaman itu

Sekarang bagian berbahaya 2 - penyaji ingin menggambar sesuatu:

  • agak tergantung pada penyaji jika kita perlu menelusuri ke model sel atau hanya dapat memberikan offset string dengan jeda baris dan attrs teks

Kelebihan:

  • aliran data yang sangat cepat
  • dioptimalkan untuk metode InputHandler paling umum - print
  • membuat garis reflow pada pengubahan ukuran terminal menjadi mungkin

Kontra:

  • hampir setiap metode InputHandler akan berbahaya dalam arti mengganggu model aliran ini dan kebutuhan beberapa abstraksi sel perantara
  • integrasi penyaji tidak jelas (setidaknya bagi saya atm)
  • mungkin menurunkan kinerja untuk kutukan seperti aplikasi (biasanya berisi lebih banyak urutan "berbahaya")

Nah ini adalah konsep kasar dari ide tersebut, jauh dari atm yang bisa digunakan karena banyak detail yang belum tercakup. khususnya bagian "berbahaya" bisa menjadi buruk dengan banyak masalah kinerja (seperti menurunkan buffer dengan perilaku gc yang lebih buruk, dll pp)

@jerch

String tidak layak untuk dimasukkan ke dalam ArrayBuffers.

Saya pikir Monaco menyimpan buffer di ArrayBuffer s dan kinerjanya cukup tinggi. Saya belum melihat terlalu dalam ke implementasinya.

khususnya mencoba menghindari perpecahan yang buruk dan bergabung

Yang mana?

Saya sudah berpikir kita harus berpikir tentang mengambil lebih banyak keuntungan dari scrollback menjadi tidak berubah entah bagaimana.

Satu ide adalah memisahkan scrollback dari bagian viewport. Setelah sebuah baris masuk ke scrollback, itu akan didorong ke dalam struktur data scrollback. Anda dapat membayangkan 2 objek CircularList , yang garisnya dioptimalkan untuk tidak pernah berubah, satu untuk sebaliknya.

@Tyriar Tentang scrollback - ya karena ini tidak pernah dapat dijangkau oleh kursor, mungkin menghemat beberapa memori untuk hanya menjatuhkan abstraksi sel untuk menggulir keluar baris.

@Tyriar
Masuk akal untuk menyimpan string di ArrayBuffer jika kita dapat membatasi konversi menjadi satu (mungkin yang terakhir untuk output render). Itu sedikit lebih baik daripada penanganan string di semua tempat. Ini dapat dilakukan karena node-pty juga dapat memberikan data mentah (dan juga soket web dapat memberi kita data mentah).

khususnya mencoba menghindari perpecahan yang buruk dan bergabung

Yang mana?

Seluruh pendekatan adalah untuk menghindari perpecahan _minimize_ sama sekali . Jika tidak ada yang meminta kursor melompat ke data buffer, string tidak akan pernah terpecah dan bisa langsung menuju ke penyaji (jika didukung). Tidak ada sel yang terbelah dan kemudian bergabung sama sekali.

@jerch baik itu bisa jika viewport diperluas, saya pikir kita juga dapat menarik scrollback ketika garis dihapus? Tidak 100% yakin tentang itu atau bahkan jika itu perilaku yang benar.

@Tyriar Ah benar. Tidak yakin tentang yang terakhir, saya pikir xterm asli memungkinkan ini hanya untuk pengguliran mouse atau scrollbar asli. Bahkan SD/SU tidak memindahkan konten scrollbuffer kembali ke viewport terminal "aktif".

Bisakah Anda mengarahkan saya ke sumber editor monaco, di mana ArrayBuffer digunakan? Sepertinya saya tidak dapat menemukannya sendiri :blush:

Hmm baca ulang spesifikasi TextEncoder/Decoder, dengan ArrayBuffers dari node-pty hingga frontend kita pada dasarnya terjebak dengan utf-8, kecuali kita menerjemahkannya dengan cara yang sulit di beberapa titik. Membuat xterm.js utf-8 sadar? Idk, ini akan melibatkan banyak perhitungan codepoint menengah untuk karakter unicode yang lebih tinggi. Proside - itu akan menghemat memori untuk karakter ascii.

@rebornix bisakah Anda memberi kami beberapa petunjuk ke mana monaco menyimpan buffer?

Berikut adalah beberapa angka untuk array yang diketik dan parser baru (lebih mudah diadopsi):

  • UTF-8 (Uint8Array): tindakan print melompat dari 190 MB/dtk menjadi 290 MB/dtk
  • UTF-16 (Uint16Array): tindakan print melompat dari 190 MB/dtk menjadi 320 MB/dtk

Secara keseluruhan UTF-16 berkinerja jauh lebih baik, tetapi itu diharapkan karena parser dioptimalkan untuk itu. UTF-8 menderita dari perhitungan codepoint perantara.

Konversi string ke array yang diketik memakan ~4% JS runtime dari benchmark saya ls -lR /usr/lib (selalu jauh di bawah 100 md, dilakukan melalui loop dalam InputHandler.parse ). Saya tidak menguji konversi terbalik (ini secara implisit dilakukan atm di InputHandller.print pada sel demi tingkat sel). Runtime keseluruhan sedikit lebih buruk daripada dengan string (waktu yang disimpan dalam parser tidak mengkompensasi waktu konversi). Ini mungkin berubah ketika bagian lain juga menyadari array yang diketik.

Dan tangkapan layar yang sesuai (diuji dengan ls -lR /usr/lib ):

dengan string:
grafik

dengan Uint16Array:
grafik

Perhatikan perbedaan untuk EscapeSequenceParser.parse , yang dapat mengambil untung dari array yang diketik (~30% lebih cepat). InputHandler.parse melakukan konversi, sehingga lebih buruk untuk versi array yang diketik. Juga GC Minor memiliki lebih banyak hal yang harus dilakukan untuk array yang diketik (karena saya membuang array).

Sunting: Aspek lain dapat dilihat di tangkapan layar - GC menjadi relevan dengan ~20% runtime, frame yang berjalan lama (berbendera merah) semuanya terkait dengan GC.

Hanya ide lain yang agak radikal:

  1. Buat memori virtual berbasis arraybuffer sendiri, sesuatu yang besar (> 5 MB)
    Jika arraybuffer memiliki panjang kelipatan 4 sakelar transparan dari int8 hingga int16 hingga int32 jenis dimungkinkan. Pengalokasi mengembalikan indeks gratis pada Uint8Array , penunjuk ini dapat dikonversi ke posisi Uint16Array atau Uint32Array dengan sedikit pergeseran sederhana.
  2. Tulis string yang masuk ke dalam memori sebagai uint16_t ketik untuk UTF-16.
  3. Parser berjalan pada pointer string dan memanggil metode di InputHandler dengan pointer ke memori ini, bukan irisan string.
  4. Buat buffer data terminal di dalam memori virtual sebagai array buffer cincin dari tipe seperti struct alih-alih objek JS asli, mungkin seperti ini (masih berbasis sel):
struct Cell {
    uint32_t *char_start;  // start pointer of cell content (JS with pointers hurray!)
    uint8_t length;        // length of content (8 bit here is sufficient)
    uint32_t attr;         // text attributes (might grow to hold true color someday)
    uint8_t width;         // wcwidth (maybe merge with other member, always < 4)
    .....                  // some other cell based stuff
}

Kelebihan:

  • menghilangkan objek JS dan dengan demikian GC jika memungkinkan (hanya beberapa objek lokal yang tersisa)
  • hanya satu salinan data awal ke dalam memori virtual yang dibutuhkan
  • hampir tidak ada biaya malloc dan free (tergantung kepintaran pengalokasi/dealokator)
  • akan menghemat banyak memori (menghindari overhead memori objek JS)

Kontra:

  • Selamat datang di Cavascript Horror Show :scream:
  • sulit diimplementasikan, mengubah segalanya
  • manfaat kecepatan tidak jelas sampai benar-benar diterapkan

:senyum:

sangat menyenangkan untuk diterapkan, mengubah segalanya

Ini lebih dekat dengan cara kerja Monaco, saya ingat posting blog ini yang membahas strategi menyimpan metadata karakter https://code.visualstudio.com/blogs/2017/02/08/syntax-highlighting-optimizations

Yup itu pada dasarnya ide yang sama.

Semoga jawaban saya tentang tempat monaco menyimpan buffer tidak terlambat.

Alex dan saya mendukung Array Buffer dan sebagian besar waktu itu memberi kami kinerja yang baik. Beberapa tempat kami menggunakan ArrayBuffer:

Kami menggunakan string sederhana untuk buffer teks alih-alih Array Buffer karena string V8 lebih mudah untuk dimanipulasi

  • Kami melakukan encoding/decoding di awal pemuatan file, sehingga file dikonversi menjadi string JS. V8 memutuskan apakah akan menggunakan satu atau dua byte untuk menyimpan karakter.
  • Kami sangat sering mengedit buffer teks, string lebih mudah ditangani.
  • Kami menggunakan modul asli nodejs dan memiliki akses ke internal V8 bila diperlukan.

Daftar berikut hanyalah ringkasan singkat dari konsep menarik yang saya temukan yang mungkin membantu menurunkan penggunaan memori dan/atau runtime:

  • FlatJS (https://github.com/lars-t-hansen/flatjs) - bahasa meta untuk membantu pengkodean dengan tumpukan berbasis arraybuffer
  • http://2ality.com/2017/01/shared-array-buffer.html (diumumkan sebagai bagian dari ES2017, masa depan mungkin tidak pasti karena Spectre, selain itu ide yang sangat menjanjikan dengan konkurensi nyata dan atom nyata)
  • webassembly/asm.js (kondisi saat ini? dapat digunakan belum? Belum mengikuti perkembangannya untuk beberapa waktu, menggunakan emscripten ke asm.js bertahun-tahun yang lalu dengan C lib untuk game AI dengan hasil yang mengesankan)
  • https://github.com/AssemblyScript/assemblyscript

Untuk mendapatkan penjumlahan di sini, berikut adalah retasan cepat bagaimana kita bisa "menggabungkan" atribut teks.

Kode ini terutama didorong oleh ide untuk menghemat memori untuk data buffer (runtime akan menderita, belum diuji berapa banyak). khususnya atribut teks dengan RGB untuk latar depan dan latar belakang (setelah didukung) akan membuat xterm.js memakan banyak memori oleh sel saat ini dengan tata letak sel. Kode mencoba untuk menghindari ini dengan penggunaan atlas penghitungan ref yang dapat diubah ukurannya untuk atribut. Ini adalah pilihan karena satu terminal hampir tidak akan menampung lebih dari 1 juta sel, yang akan menumbuhkan atlas menjadi 1M * entry_size jika semua sel berbeda.

Sel itu sendiri hanya perlu menyimpan indeks atlas atribut. Pada perubahan sel, indeks lama harus tidak direferensikan dan yang baru direferensikan. Indeks atlas akan menggantikan atribut atribut dari objek terminal dan akan berubah sendiri di SGR.

Atlas saat ini hanya membahas atribut teks, tetapi dapat diperluas ke semua atribut sel jika diperlukan. Sementara buffer terminal saat ini menampung 2 angka 32bit untuk data atribut (4 dengan RGB dalam desain buffer saat ini), atlas akan menguranginya menjadi hanya satu angka 32bit. Entri atlas dapat dikemas lebih lanjut juga.

interface TextAttributes {
    flags: number;
    foreground: number;
    background: number;
}

const enum AtlasEntry {
    FLAGS = 1,
    FOREGROUND = 2,
    BACKGROUND = 3
}

class TextAttributeAtlas {
    /** data storage */
    private data: Uint32Array;
    /** flag lookup tree, not happy with that yet */
    private flagTree: any = {};
    /** holds freed slots */
    private freedSlots: number[] = [];
    /** tracks biggest idx to shortcut new slot assignment */
    private biggestIdx: number = 0;
    constructor(size: number) {
        this.data = new Uint32Array(size * 4);
    }
    private setData(idx: number, attributes: TextAttributes): void {
        this.data[idx] = 0;
        this.data[idx + AtlasEntry.FLAGS] = attributes.flags;
        this.data[idx + AtlasEntry.FOREGROUND] = attributes.foreground;
        this.data[idx + AtlasEntry.BACKGROUND] = attributes.background;
        if (!this.flagTree[attributes.flags])
            this.flagTree[attributes.flags] = [];
        if (this.flagTree[attributes.flags].indexOf(idx) === -1)
            this.flagTree[attributes.flags].push(idx);
    }

    /**
     * convenient method to inspect attributes at slot `idx`.
     * For better performance atlas idx and AtlasEntry
     * should be used directly to avoid number conversions.
     * <strong i="10">@param</strong> {number} idx
     * <strong i="11">@return</strong> {TextAttributes}
     */
    getAttributes(idx: number): TextAttributes {
        return {
            flags: this.data[idx + AtlasEntry.FLAGS],
            foreground: this.data[idx + AtlasEntry.FOREGROUND],
            background: this.data[idx + AtlasEntry.BACKGROUND]
        };
    }

    /**
     * Returns a slot index in the atlas for the given text attributes.
     * To be called upon attributes changes, e.g. by SGR.
     * NOTE: The ref counter is set to 0 for a new slot index, thus
     * values will get overwritten if not referenced in between.
     * <strong i="12">@param</strong> {TextAttributes} attributes
     * <strong i="13">@return</strong> {number}
     */
    getSlot(attributes: TextAttributes): number {
        // find matching attributes slot
        const sameFlag = this.flagTree[attributes.flags];
        if (sameFlag) {
            for (let i = 0; i < sameFlag.length; ++i) {
                let idx = sameFlag[i];
                if (this.data[idx + AtlasEntry.FOREGROUND] === attributes.foreground
                    && this.data[idx + AtlasEntry.BACKGROUND] === attributes.background) {
                    return idx;
                }
            }
        }
        // try to insert into a previously freed slot
        const freed = this.freedSlots.pop();
        if (freed) {
            this.setData(freed, attributes);
            return freed;
        }
        // else assign new slot
        for (let i = this.biggestIdx; i < this.data.length; i += 4) {
            if (!this.data[i]) {
                this.setData(i, attributes);
                if (i > this.biggestIdx)
                    this.biggestIdx = i;
                return i;
            }
        }
        // could not find a valid slot --> resize storage
        const data = new Uint32Array(this.data.length * 2);
        for (let i = 0; i < this.data.length; ++i)
            data[i] = this.data[i];
        const idx = this.data.length;
        this.data = data;
        this.setData(idx, attributes);
        return idx;
    }

    /**
     * Increment ref counter.
     * To be called for every terminal cell, that holds `idx` as text attributes.
     * <strong i="14">@param</strong> {number} idx
     */
    ref(idx: number): void {
        this.data[idx]++;
    }

    /**
     * Decrement ref counter. Once dropped to 0 the slot will be reused.
     * To be called for every cell that gets removed or reused with another value.
     * <strong i="15">@param</strong> {number} idx
     */
    unref(idx: number): void {
        this.data[idx]--;
        if (!this.data[idx]) {
            let treePart = this.flagTree[this.data[idx + AtlasEntry.FLAGS]];
            treePart.splice(treePart.indexOf(this.data[idx]), 1);
        }
    }
}

let atlas = new TextAttributeAtlas(2);
let a1 = atlas.getSlot({flags: 12, foreground: 13, background: 14});
atlas.ref(a1);
// atlas.unref(a1);
let a2 = atlas.getSlot({flags: 12, foreground: 13, background: 15});
atlas.ref(a2);
let a3 = atlas.getSlot({flags: 13, foreground: 13, background: 16});
atlas.ref(a3);
let a4 = atlas.getSlot({flags: 13, foreground: 13, background: 16});
console.log(atlas);
console.log(a1, a2, a3, a4);
console.log('a1', atlas.getAttributes(a1));
console.log('a2', atlas.getAttributes(a2));
console.log('a3', atlas.getAttributes(a3));
console.log('a4', atlas.getAttributes(a4));

Sunting:
Penalti runtime hampir nol, untuk benchmark saya dengan ls -lR /usr/lib itu menambahkan kurang dari 1 msec ke total runtime ~2,3 s. Catatan tambahan yang menarik - perintah menetapkan kurang dari 64 slot atribut teks yang berbeda untuk output data sebesar 5 MB dan akan menghemat lebih dari 20 MB setelah diterapkan sepenuhnya.

Membuat beberapa PR prototipe untuk menguji beberapa perubahan pada buffer (lihat https://github.com/xtermjs/xterm.js/pull/1528#issue-196949371 untuk gagasan umum di balik perubahan):

  • PR #1528 : atlas atribut
  • PR #1529 : hapus wcwidth dan charCode dari buffer
  • PR #1530 : ganti string dalam buffer dengan codepoint / nilai indeks penyimpanan sel

@jerch mungkin ide yang baik untuk menjauh dari kata atlas untuk ini sehingga "atlas" selalu berarti "atlas tekstur". Sesuatu seperti toko atau cache mungkin akan lebih baik?

oh ok, "cache" baik-baik saja.

Kira saya sudah selesai dengan PR testbed. Silakan lihat juga komentar PR untuk mendapatkan latar belakang ringkasan kasar berikut.

Usul:

  1. Bangun AttributeCache untuk menampung semua yang diperlukan untuk menata satu sel terminal. Lihat #1528 untuk versi penghitungan ref awal yang juga dapat menyimpan spesifikasi warna sebenarnya. Cache juga dapat dibagikan di antara instance terminal yang berbeda jika diperlukan untuk menyimpan memori lebih lanjut di beberapa aplikasi terminal.
  2. Bangun StringStorage untuk menampung string data konten terminal pendek. Versi di #1530 bahkan menghindari penyimpanan string char tunggal dengan "membebani" arti pointer. wcwidth harus dipindahkan ke sini.
  3. Kecilkan CharData dari [number, string, number, number] menjadi [number, number] , di mana angkanya adalah pointer (nomor indeks) ke:

    • AttributeCache entri

    • StringStorage entri

Atribut tidak mungkin banyak berubah, jadi satu nomor 32bit akan menghemat banyak memori dari waktu ke waktu. Penunjuk StringStorage adalah titik kode unicode nyata untuk karakter tunggal, oleh karena itu dapat digunakan sebagai entri code dari CharData . String sebenarnya dapat diakses oleh StringStorage.getString(idx) . Bidang keempat wcwidth dari CharData dapat diakses oleh StringStorage.wcwidth(idx) (belum diimplementasikan). Hampir tidak ada hukuman runtime dari menyingkirkan code dan wcwidth di CharData (diuji di #1529).

  1. Pindahkan menyusut CharData menjadi padat Int32Array pelaksanaan penyangga berdasarkan. Juga diuji di #1530 dengan kelas rintisan (jauh dari berfungsi penuh), manfaat akhir kemungkinan adalah:

    • Jejak memori 80% lebih kecil dari buffer terminal (dari 5,5 MB hingga 0,75 MB)

    • sedikit lebih cepat (belum dapat diuji, saya berharap mendapatkan kecepatan 20% - 30%)

    • Sunting: jauh lebih cepat - runtime skrip untuk ls -lR /usr/lib turun menjadi 1,3 detik (master pada 2,1 detik) sementara buffer lama masih aktif untuk penanganan kursor, setelah dihapus, saya berharap runtime turun di bawah 1 detik

Kelemahan - langkah 4 cukup banyak bekerja karena akan memerlukan beberapa pengerjaan ulang pada antarmuka buffer. Tapi hei - untuk menghemat 80% RAM dan tetap mendapatkan kinerja runtime, itu bukan masalah besar, bukan? :senyum:

Ada masalah lain yang saya temukan - representasi sel kosong saat ini. Imho sel dapat memiliki 3 status:

  • kosong : status sel awal, belum ada yang ditulis atau konten telah dihapus. Ini memiliki lebar 1 tetapi tidak ada konten. Saat ini digunakan di blankLine dan eraseChar , tetapi dengan spasi sebagai konten.
  • null : sel setelah karakter lebar penuh untuk menunjukkan, bahwa ia tidak memiliki lebar untuk representasi visual.
  • normal : sel menampung beberapa konten dan memiliki lebar visual (1 atau 2, mungkin lebih besar setelah kami mendukung hal-hal grapheme/bidi nyata, belum yakin tentang itu lol)

Masalah yang saya lihat di sini adalah bahwa sel kosong tidak dapat dibedakan dari sel normal dengan spasi yang dimasukkan, keduanya terlihat sama pada tingkat buffer (konten yang sama, lebar yang sama). Saya tidak menulis kode penyaji/output apa pun tetapi saya berharap ini mengarah pada situasi canggung di bagian depan output. khususnya penanganan ujung kanan garis mungkin menjadi rumit.
Terminal dengan 15 cols, pertama beberapa output string, yang dililitkan:

1: 'H', 'e', 'l', 'l', 'o', ' ', 't', 'e', 'r', 'm', 'i', 'n', 'a', 'l', ' '
2: 'w', 'o', 'r', 'l', 'd', '!', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '

versus daftar folder dengan ls :

1: 'R', 'e', 'a', 'd', 'm', 'e', '.', 'm', 'd', ' ', ' ', ' ', ' ', ' ', ' '
2: 'f', 'i', 'l', 'e', 'A', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '

Contoh pertama berisi spasi nyata setelah kata 'terminal', contoh kedua tidak pernah menyentuh sel setelah 'Readme.md'. Cara itu diwakili pada tingkat buffer masuk akal untuk kasus standar untuk mencetak barang-barang sebagai output terminal ke layar (ruangan perlu diambil lagian), tetapi untuk alat yang mencoba menangani string konten seperti pemilihan mouse atau manajer reflow tidak jelas lagi, dari mana ruang itu berasal.

Kurang lebih ini mengarah ke pertanyaan berikutnya - bagaimana menentukan panjang konten aktual dalam satu baris (jumlah sel yang berisi sesuatu dari sisi kiri)? Pendekatan sederhana akan menghitung sel kosong dari sisi kanan, sekali lagi makna ganda dari atas membuat ini sulit untuk ditentukan.

Usul:
Imho ini mudah diperbaiki dengan menggunakan beberapa placeholder lain untuk sel kosong, misalnya karakter kontrol atau string kosong dan menggantinya dalam proses render jika diperlukan. Mungkin penyaji layar juga bisa mendapatkan keuntungan dari ini, karena mungkin tidak harus menangani sel-sel itu sama sekali (tergantung pada cara output dihasilkan).

Btw, untuk string yang dibungkus di atas ini juga mengarah ke masalah isWrapped , yang penting untuk mengubah ukuran atau penanganan pemilihan salin&tempel yang benar. Imho kita tidak bisa menghapusnya, tetapi perlu mengintegrasikannya lebih baik daripada atm.

@jerch kerja yang mengesankan! :senyum:

1 Bangun AttributeCache untuk menampung semua yang diperlukan untuk menata satu sel terminal. Lihat #1528 untuk versi penghitungan ref awal yang juga dapat menyimpan spesifikasi warna sebenarnya. Cache juga dapat dibagikan di antara instance terminal yang berbeda jika diperlukan untuk menyimpan memori lebih lanjut di beberapa aplikasi terminal.

Membuat beberapa komentar di #1528.

2 Bangun StringStorage untuk menampung string data konten terminal pendek. Versi di #1530 bahkan menghindari penyimpanan string char tunggal dengan "membebani" arti pointer. wcwidth harus dipindahkan ke sini.

Membuat beberapa komentar di #1530.

4 Pindahkan CharData yang menyusut ke dalam implementasi buffer berbasis Int32Array yang padat. Juga diuji di #1530 dengan kelas rintisan (jauh dari berfungsi penuh), manfaat akhir kemungkinan adalah:

Belum sepenuhnya terjual pada ide ini, saya pikir itu akan menggigit kita ketika kita menerapkan reflow. Tampaknya masing-masing langkah ini dapat dilakukan secara berurutan sehingga kita dapat melihat bagaimana keadaan berjalan dan melihat apakah masuk akal untuk melakukan ini setelah kita menyelesaikan 3 langkah.

Ada masalah lain yang saya temukan - representasi sel kosong saat ini. Imho sel dapat memiliki 3 status

Berikut ini contoh bug yang keluar dari https://github.com/xtermjs/xterm.js/issues/1286 , :+1: untuk membedakan sel spasi putih dan sel "kosong"

Btw, untuk string yang dibungkus di atas ini juga mengarah ke masalah isWrapped, yang penting untuk mengubah ukuran atau penanganan pemilihan salin&tempel yang benar. Imho kita tidak bisa menghapusnya, tetapi perlu mengintegrasikannya lebih baik daripada atm.

Saya melihat isWrapped akan hilang ketika kami menangani https://github.com/xtermjs/xterm.js/issues/622 karena CircularList hanya akan berisi baris yang tidak terbungkus.

Belum sepenuhnya terjual pada ide ini, saya pikir itu akan menggigit kita ketika kita menerapkan reflow. Tampaknya masing-masing langkah ini dapat dilakukan secara berurutan sehingga kita dapat melihat bagaimana keadaan berjalan dan melihat apakah masuk akal untuk melakukan ini setelah kita menyelesaikan 3 langkah.

Ya, saya bersamamu (masih menyenangkan untuk bermain-main dengan pendekatan yang sama sekali berbeda ini). 1 dan 2 dapat dipilih, 3 dapat diterapkan tergantung pada 1 atau 2. 4 adalah opsional, kita dapat tetap berpegang pada tata letak buffer saat ini. Penghematan memori seperti ini:

  1. 1 + 2 + 3 di CircularList : menghemat 50% (~2.8 MB dari ~5.5 MB)
  2. 1 + 2 + 3 + 4 setengah - cukup masukkan data baris dalam array yang diketik tetapi tetap pada akses indeks baris: menghemat 82% (~ 0,9 MB)
  3. 1 + 2 + 3 + 4 array padat penuh dengan aritmatika pointer: menghemat 87% (~ 0,7 MB)

1. sangat mudah diimplementasikan, perilaku memori dengan scrollBack yang lebih besar masih akan menunjukkan penskalaan yang buruk seperti yang ditunjukkan di sini https://github.com/xtermjs/xterm.js/pull/1530#issuecomment -403542479 tetapi pada tingkat yang kurang beracun
2. Sedikit lebih sulit untuk diterapkan (beberapa tipuan lagi di tingkat baris diperlukan), tetapi akan memungkinkan untuk menjaga API yang lebih tinggi dari Buffer tetap utuh. Imho pilihan untuk pergi - simpan mem besar dan masih mudah untuk diintegrasikan.
3. 5% lebih banyak mem save daripada opsi 2, sulit diterapkan, akan mengubah menyentuh semua API dan dengan demikian secara harfiah seluruh basis kode. Imho lebih dari kepentingan akademis atau untuk hari-hari hujan yang membosankan untuk diterapkan lol.

@Tyriar Saya melakukan beberapa tes lebih lanjut dengan rust untuk penggunaan webassembly dan menulis ulang parser. Perhatikan bahwa keterampilan karat saya agak "berkarat" karena saya belum mendalaminya, oleh karena itu berikut ini mungkin hasil dari kode karat yang lemah. Hasil:

  • Penanganan data di bagian wasm sedikit lebih cepat (5 - 10%).
  • Panggilan dari JS ke wasm membuat beberapa overhead dan memakan semua manfaat dari atas. Bahkan itu ~ 20% lebih lambat.
  • "Biner" akan lebih kecil dari mitra JS (tidak benar-benar diukur karena saya tidak mengimplementasikan semua hal).
  • Untuk mendapatkan JS <--> transisi wasm dengan mudah dilakukan beberapa bloatcode diperlukan untuk menangani tipe JS (hanya melakukan terjemahan string).
  • Kami tidak dapat menghindari terjemahan JS ke wasm karena DOM browser dan acara tidak dapat diakses di sana. Itu hanya dapat digunakan untuk bagian inti, yang kinerjanya tidak terlalu kritis lagi (selain konsumsi mem).

Kecuali jika kita ingin menulis ulang seluruh lib inti dalam rust (atau bahasa lain yang mampu wasm), kita tidak dapat memperoleh apa pun dari pindah ke wasm lang imho. Kelebihan dari wasm langs saat ini adalah kenyataan bahwa sebagian besar mendukung penanganan memori eksplisit (dapat membantu kami dengan masalah buffer), kerugiannya adalah pengenalan bahasa yang sama sekali berbeda ke dalam proyek yang berfokus pada TS/JS (penghalang tinggi untuk penambahan kode) dan biaya translasi antara wasm dan JS land.

TL;DR
xterm.js adalah untuk secara luas ke hal-hal JS umum seperti DOM dan acara untuk mendapatkan apa pun dari webassembly bahkan untuk menulis ulang bagian inti.

@jerch investigasi yang bagus :smiley:

Panggilan dari JS ke wasm membuat beberapa overhead dan memakan semua manfaat dari atas. Bahkan itu ~ 20% lebih lambat.

Ini adalah masalah utama untuk monaco yang menjadi asli juga yang terutama menginformasikan pendirian saya (meskipun itu dengan modul simpul asli, bukan wasm). Saya percaya bekerja dengan ArrayBuffer s di mana pun memungkinkan akan memberi kita keseimbangan terbaik antara kinerja dan kesederhanaan (mudah impl, penghalang masuk).

@Tyriar Akan mencoba membuat AttributeStorage untuk menyimpan data RGB. Belum yakin tentang BST, untuk kasus penggunaan tipikal dengan hanya beberapa pengaturan warna dalam sesi terminal, ini akan menjadi lebih buruk di runtime, mungkin ini harus menjadi runtime drop-in setelah warna melampaui batas yang diberikan. Juga konsumsi memori akan meningkat banyak lagi, meskipun masih akan menghemat memori karena atribut disimpan hanya sekali dan tidak bersama dengan setiap sel (skenario terburuk dengan setiap sel yang memiliki atribut berbeda akan menderita).
Tahukah Anda mengapa nilai warna fg dan bg 256 warna saat ini berbasis 9 bit, bukan 8 bit? Untuk apa bit tambahan digunakan? Di sini: https://github.com/xtermjs/xterm.js/blob/6691f809069a549b4808cd2e055398d2da15db37/src/InputHandler.ts#L1596
Bisakah Anda memberi saya tata letak bit saat ini attr ? Saya pikir pendekatan serupa seperti "makna ganda" untuk pointer StringStorage dapat lebih menghemat memori tetapi itu akan membutuhkan MSB attr untuk dicadangkan untuk perbedaan pointer dan tidak digunakan untuk tujuan lain. Ini mungkin membatasi kemungkinan untuk mendukung flag atribut lebih lanjut di kemudian hari (karena FLAGS sudah menggunakan 7 bit), apakah kita masih kehilangan beberapa flag fundamental yang kemungkinan akan datang?

Angka 32 bit attr dalam istilah buffer dapat dikemas seperti ini:

# 256 indexed colors
32:       0 (no RGB color)
31..25:   flags (7 bits)
24..17:   fg (8 bits, see question above)
16..9:    bg
8..1:     unused

# RGB colors
32:       1 (RGB color)
31..25:   flags (7 bits)
24..1:    pointer to RGB data (address space is 2^24, which should be sufficient)

Dengan cara ini penyimpanan hanya perlu menyimpan data RGB dalam dua angka 32 bit sementara flag dapat tetap berada di angka attr .

@jerch ngomong -ngomong saya mengirimi Anda email, mungkin dimakan oleh filter spam lagi

Tahukah Anda mengapa nilai fg dan bg 256 warna saat ini berbasis 9 bit, bukan 8 bit? Untuk apa bit tambahan digunakan?

Saya pikir ini digunakan untuk warna fg/bg default (yang bisa gelap atau terang), jadi sebenarnya 257 warna.

https://github.com/xtermjs/xterm.js/pull/756/files

Bisakah Anda memberi saya sedikit tata letak attr saat ini?

Saya pikir ini:

19+:     flags (see `FLAGS` enum)
18..18:  default fg flag
17..10:  256 fg
9..9:    default bg flag
8..1:    256 bg

Anda dapat melihat apa yang saya dapatkan untuk truecolor di PR lama https://github.com/xtermjs/xterm.js/pull/756/files :

/**
 * Character data, the array's format is:
 * - string: The character.
 * - number: The width of the character.
 * - number: Flags that decorate the character.
 *
 *        truecolor fg
 *        |   inverse
 *        |   |   underline
 *        |   |   |
 *   0b 0 0 0 0 0 0 0
 *      |   |   |   |
 *      |   |   |   bold
 *      |   |   blink
 *      |   invisible
 *      truecolor bg
 *
 * - number: Foreground color. If default bit flag is set, color is the default
 *           (inherited from the DOM parent). If truecolor fg flag is true, this
 *           is a 24-bit color of the form 0xxRRGGBB, if not it's an xterm color
 *           code ranging from 0-255.
 *
 *        red
 *        |       blue
 *   0x 0 R R G G B B
 *      |     |
 *      |     green
 *      default color bit
 *
 * - number: Background color. The same as foreground color.
 */
export type CharData = [string, number, number, number, number];

Jadi dalam hal ini saya memiliki 2 bendera; satu untuk warna default (apakah mengabaikan semua bit warna) dan satu untuk warna asli (apakah akan melakukan 256 atau 16 juta warna).

Ini mungkin membatasi kemungkinan untuk mendukung flag atribut lebih lanjut di kemudian hari (karena FLAGS sudah menggunakan 7 bit), apakah kita masih kehilangan beberapa flag fundamental yang kemungkinan akan datang?

Ya, kami menginginkan ruang untuk flag tambahan, misalnya https://github.com/xtermjs/xterm.js/issues/580, https://github.com/xtermjs/xterm.js/issues/1145, saya akan katakan setidaknya tinggalkan> 3 bit jika memungkinkan.

Alih-alih data penunjuk di dalam attr itu sendiri, mungkin ada peta lain yang menyimpan referensi ke data rgb? mapAttrIdxToRgb: { [idx: number]: RgbData

@Tyriar Maaf, beberapa hari tidak online dan saya khawatir emailnya dimakan oleh filter spam. Bisa tolong kirim ulang? :merah:

Dimainkan sedikit dengan struktur data pencarian yang lebih pintar untuk penyimpanan attrs. Yang paling menjanjikan mengenai ruang dan runtime search/insert adalah tree dan skiplist sebagai alternatif yang lebih murah. Secara teori loh. Dalam praktiknya, keduanya tidak dapat mengungguli pencarian array sederhana saya yang tampaknya sangat aneh bagi saya (bug dalam kode di suatu tempat?)
Saya mengunggah file uji di sini https://Gist.github.com/jerch/ff65f3fb4414ff8ac84a947b3a1eec58 dengan array vs. pohon merah-hitam yang condong ke kiri, yang menguji hingga 10 juta entri (yang hampir merupakan ruang pengalamatan lengkap). Masih jauh di depan dibandingkan dengan LLRB, saya menduga titik impasnya sekitar 10 juta. Diuji pada laptop lama saya yang berusia 7 tahun, mungkin seseorang dapat mengujinya juga dan bahkan lebih baik - arahkan saya ke beberapa bug di impl/tes.

Berikut adalah beberapa hasil (dengan angka yang berjalan):

prefilled             time for inserting 1000 * 1000 (summed up, ms)
items                 array        LLRB
100-10000             3.5 - 5      ~13
100000                ~12          ~15
1000000               8            ~18
10000000              20-25        21-28

Apa yang benar-benar mengejutkan saya adalah kenyataan bahwa pencarian array linier tidak menunjukkan pertumbuhan di daerah yang lebih rendah sama sekali, hingga 10k entri stabil pada ~ 4ms (mungkin terkait cache). Tes 10M menunjukkan runtime yang lebih buruk dari yang diharapkan, mungkin karena mem paging apa pun. Mungkin JS jauh dari mesin dengan JIT dan semua opts/deopts terjadi, tetap saja saya pikir mereka tidak dapat menghilangkan langkah kompleksitas (meskipun LLRB tampaknya berat pada satu _n_ sehingga memindahkan titik impas untuk O( n) vs. O(logn) ke atas)

Btw dengan data acak perbedaannya bahkan lebih buruk.

Saya pikir ini digunakan untuk warna fg/bg default (yang bisa gelap atau terang), jadi sebenarnya 257 warna.

Jadi ini membedakan SGR 39 atau SGR 49 dari salah satu dari 8 warna palet?

Alih-alih data penunjuk di dalam attr itu sendiri, mungkin ada peta lain yang menyimpan referensi ke data rgb? mapAttrIdxToRgb: { [idx: nomor]: RgbData

Ini akan memperkenalkan tipuan lain dengan penggunaan memori tambahan. Dengan pengujian di atas saya juga menguji perbedaan antara selalu memegang bendera di attrs vs menyimpannya bersama dengan data RGB di penyimpanan. Karena perbedaannya adalah ~ 0,5 ms untuk entri 1M, saya tidak akan menggunakan pengaturan attrs yang rumit ini, alih-alih menyalin flag ke penyimpanan setelah RGB diatur. Masih saya akan memilih perbedaan bit ke-32 antara attrs langsung vs pointer karena ini akan menghindari penyimpanan sama sekali untuk sel non RGB.

Juga saya pikir 8 warna palet default untuk fg/bg tidak cukup terwakili dalam buffer saat ini. Secara teori terminal harus mendukung mode warna berikut:

  1. SGR 39 + SGR 49 warna default untuk fg/bg (dapat disesuaikan)
  2. SGR 30-37 + SGR 40-47 8 palet warna rendah untuk fg/bg (dapat disesuaikan)
  3. SGR 90-97 + SGR 100-107 8 palet warna tinggi untuk fg/bg (dapat disesuaikan)
  4. SGR 38;5;n + SGR 48;5;n 256 palet terindeks untuk fg/bg (dapat disesuaikan)
  5. SGR 38;2;r;g;b + SGR 48;2;r;g;b RGB untuk fg/bg (tidak dapat disesuaikan)

Opsi 2.) dan 3.) dapat digabungkan menjadi satu byte (memperlakukannya sebagai satu palet 16 warna fg/bg), 4.) membutuhkan 2 byte dan 5.) akhirnya akan membutuhkan 6 byte lagi. Kami masih membutuhkan beberapa bit untuk menunjukkan mode warna.
Untuk mencerminkan hal ini pada level buffer, kita memerlukan yang berikut:

bits        for
2           fg color mode (0: default, 1: 16 palette, 2: 256, 3: RGB)
2           bg color mode (0: default, 1: 16 palette, 2: 256, 3: RGB)
8           fg color for 16 palette and 256
8           bg color for 16 palette and 256
10          flags (currently 7, 3 more reserved for future usage)
----
30

Jadi kita membutuhkan 30 bit dari angka 32 bit, meninggalkan 2 bit gratis untuk keperluan lain. Bit ke-32 dapat menahan penunjuk vs. flag attr langsung menghilangkan penyimpanan untuk sel non RGB.

Saya juga menyarankan untuk membungkus akses attr ke dalam kelas yang nyaman untuk tidak mengekspos detail implementasi ke luar (lihat file pengujian di atas, ada versi awal dari kelas TextAttributes untuk mencapai ini).

Maaf, beberapa hari tidak online dan saya khawatir emailnya dimakan oleh filter spam. Bisa tolong kirim ulang?

Membenci

Oh btw angka-angka di atas untuk pencarian array vs llrb adalah omong kosong - saya pikir itu dimanjakan oleh pengoptimal yang melakukan beberapa hal aneh di for loop. Dengan pengaturan pengujian yang sedikit berbeda, ini dengan jelas menunjukkan O(n) vs. O(log n) tumbuh jauh lebih awal (dengan 1000 elemen yang telah diisi sebelumnya sudah lebih cepat dengan pohon).

Kondisi saat ini:

Setelah:

Salah satu optimasi yang cukup sederhana adalah dengan meratakan array-of-array menjadi satu array. Yaitu alih-alih BufferLine dari _N_ kolom yang memiliki array _data dari _N_ CharData sel, di mana setiap CharData adalah array 4, hanya memiliki satu array elemen _4*N_. Ini menghilangkan overhead objek dari array _N_. Ini juga meningkatkan lokalitas cache, jadi seharusnya lebih cepat. Kerugiannya adalah kode yang sedikit lebih rumit dan lebih buruk, tetapi tampaknya sepadan.

Sebagai tindak lanjut dari komentar saya sebelumnya, tampaknya perlu dipertimbangkan untuk menggunakan sejumlah variabel elemen dalam array _data untuk setiap sel. Dengan kata lain representasi stateful. Perubahan acak dalam posisi akan lebih mahal, tetapi pemindaian linier dari awal baris bisa sangat cepat, terutama karena array sederhana dioptimalkan untuk lokalitas cache. Output sekuensial tipikal akan cepat, seperti halnya rendering.

Selain mengurangi ruang, keuntungan dari sejumlah variabel elemen per sel adalah peningkatan fleksibilitas: Atribut ekstra (seperti warna 24-bit), anotasi untuk sel atau rentang tertentu, mesin terbang atau
elemen DOM bersarang.

@PerBothner Terima kasih atas ide-ide Anda! Ya saya sudah menguji tata letak array padat tunggal dengan aritmatika pointer, ini menunjukkan pemanfaatan memori terbaik. Masalah muncul ketika mengubah ukuran, itu pada dasarnya berarti membangun kembali seluruh potongan memori (salin) atau menyalin cepat ke potongan yang lebih besar dan menyelaraskan kembali bagian. Ini cukup exp. dan saya tidak dibenarkan oleh penghematan mem (diuji di beberapa PR taman bermain yang tercantum di atas, penghematannya sekitar ~ 10% dibandingkan dengan implementasi garis buffer baru).

Tentang komentar kedua Anda - kami sudah membahasnya karena akan membuat penanganan garis yang dibungkus lebih mudah. Untuk saat ini kami memutuskan untuk menggunakan pendekatan baris X col untuk tata letak buffer baru dan menyelesaikannya terlebih dahulu. Saya pikir kita harus mengatasi ini lagi setelah kita melakukan implementasi reflow resize.

Tentang menambahkan hal-hal tambahan ke buffer: saat ini kami melakukan di sini apa yang dilakukan kebanyakan terminal lain - gerak maju kursor ditentukan oleh wcwidth yang memastikan untuk tetap kompatibel dengan ide pty/termios bagaimana data harus ditata. Ini pada dasarnya berarti bahwa kami menangani pada level buffer hanya hal-hal seperti pasangan pengganti dan menggabungkan karakter. Aturan bergabung "tingkat lebih tinggi" lainnya dapat diterapkan oleh penggabung karakter di penyaji (saat ini digunakan oleh https://github.com/xtermjs/xterm-addon-ligatures untuk ligatur). Saya memiliki PR terbuka untuk juga mendukung grafem unicode pada tingkat buffer awal tetapi saya pikir kami tidak dapat melakukan ini pada tahap itu karena sebagian besar backend pty tidak memiliki gagasan tentang ini (apakah ada sama sekali?) dan kami akan berakhir dengan konglomerat char yang aneh . Hal yang sama berlaku untuk dukungan BIDI nyata, saya pikir grafem dan BIDI lebih baik dilakukan pada tahap penyaji untuk menjaga pergerakan kursor/sel tetap utuh.

Dukungan untuk node DOM yang dilampirkan ke sel terdengar sangat menarik, saya suka ide itu. Saat ini itu tidak mungkin dalam pendekatan langsung karena kami memiliki backend perender yang berbeda (DOM, kanvas 2D, dan perender webgl baru yang mengkilap), saya pikir ini masih dapat dicapai untuk semua perender dengan memposisikan overlay di tempat yang tidak didukung secara asli (hanya perender DOM yang akan melakukannya dapat melakukannya secara langsung). Kami akan membutuhkan semacam API pada tingkat buffer untuk mengumumkan hal itu dan ukurannya dan penyaji dapat melakukan pekerjaan kotor. Saya pikir kita harus mendiskusikan / melacak ini dengan masalah terpisah.

Terima kasih atas tanggapan terperinci Anda.

_"Masalah muncul ketika mengubah ukuran, itu pada dasarnya berarti membangun kembali seluruh potongan memori (salin) atau menyalin cepat ke potongan yang lebih besar dan menyelaraskan kembali bagian."_

Maksud Anda: saat mengubah ukuran, kita harus menyalin elemen _4*N_ daripada hanya elemen _N_?

Mungkin masuk akal jika array berisi semua sel untuk baris logis (tidak terbungkus). Misalnya, asumsikan garis 180 karakter dan terminal lebar 80 kolom. Dalam hal ini Anda dapat memiliki 3 instance BufferLine semuanya berbagi buffer _4*180_-element _data , tetapi setiap BufferLine juga akan berisi offset awal.

Yah saya memiliki semuanya dalam satu array besar yang dibangun oleh [cols] x [rows] x [needed single cell space] . Jadi pada dasarnya masih berfungsi sebagai "kanvas" dengan tinggi dan lebar tertentu. Ini benar-benar hemat memori dan cepat untuk aliran input normal, tetapi segera setelah insertCell / deleteCell dipanggil (pengubahan ukuran akan melakukannya), seluruh memori di belakang posisi tempat tindakan berlangsung harus digeser. Untuk scrollback kecil (<10k) ini bahkan bukan masalah, ini benar-benar merupakan showstopper untuk> 100k baris.
Perhatikan impl array yang diketik saat ini masih harus melakukan perubahan itu, tetapi kurang beracun karena hanya perlu memindahkan konten memori hingga akhir baris.
Saya memikirkan tata letak yang berbeda untuk menghindari pergeseran yang mahal, bidang utama untuk menghemat pergeseran memori yang tidak masuk akal adalah dengan benar-benar memisahkan scrollback dari "baris terminal panas" (yang terbaru hingga terminal.rows ) karena hanya itu yang bisa diubah oleh kursor melompat dan menyisipkan/menghapus.

Berbagi memori yang mendasarinya dengan beberapa objek garis penyangga adalah ide yang menarik untuk memecahkan masalah pembungkus. Belum yakin bagaimana ini dapat bekerja dengan andal tanpa memperkenalkan penanganan ref eksplisit dan semacamnya. Di versi lain saya mencoba melakukan semuanya dengan penanganan memori eksplisit, tetapi penghitung ref adalah penghenti nyata dan merasa salah di tanah GC. (lihat #1633 untuk primitif)

Sunting: Btw penanganan memori eksplisit setara dengan pendekatan "memori per baris" saat ini, saya berharap kinerja yang lebih baik karena lokalitas cache yang lebih baik, kira itu dimakan oleh exp yang sedikit lebih banyak. mem penanganan dalam abstraksi JS.

Dukungan untuk node DOM yang dilampirkan ke sel terdengar sangat menarik, saya suka ide itu. Saat ini itu tidak mungkin dalam pendekatan langsung karena kami memiliki backend perender yang berbeda (DOM, kanvas 2D, dan perender webgl baru yang mengkilap), saya pikir ini masih dapat dicapai untuk semua perender dengan memposisikan overlay di tempat yang tidak didukung secara asli (hanya perender DOM yang akan melakukannya dapat melakukannya secara langsung).

Ini agak keluar dari topik tapi saya melihat kami akhirnya memiliki node DOM yang terkait dengan sel di dalam viewport yang akan bertindak mirip dengan lapisan render kanvas. Dengan begitu konsumen akan dapat "menghias" sel menggunakan HTML dan CSS dan tidak perlu masuk ke kanvas API.

Mungkin masuk akal jika array berisi semua sel untuk baris logis (tidak terbungkus). Misalnya, asumsikan garis 180 karakter dan terminal lebar 80 kolom. Dalam hal ini Anda dapat memiliki 3 instance BufferLine yang semuanya berbagi buffer _data 4*180-elemen yang sama, tetapi setiap BufferLine juga akan berisi offset awal.

Rencana reflow yang disebutkan di atas ditangkap di https://github.com/xtermjs/xterm.js/issues/622#issuecomment -375403572 , pada dasarnya kami ingin memiliki buffer yang sebenarnya tidak terbungkus dan kemudian tampilan di atas yang mengelola baris baru untuk akses cepat ke baris mana pun (juga mengoptimalkan pengubahan ukuran horizontal).

Menggunakan pendekatan array padat mungkin sesuatu yang bisa kita lihat, tetapi sepertinya itu tidak sepadan dengan biaya tambahan dalam mengelola jeda baris yang tidak terbungkus dalam array seperti itu, dan kekacauan yang muncul ketika baris dipangkas dari atas penyangga gulir balik. Terlepas dari itu, saya tidak berpikir kita harus melihat perubahan seperti itu sampai #791 selesai dan kita melihat #622.

Dengan PR #1796 parser mendapat dukungan array yang diketik yang membuka pintu untuk optimasi lebih lanjut serveral, di sisi lain juga untuk pengkodean input lainnya.

Untuk saat ini saya memutuskan untuk menggunakan Uint16Array , karena mudah bolak-balik dapat dikonversi dengan string JS. Ini pada dasarnya membatasi permainan ke UCS2/UTF16, sedangkan parser dalam versi saat ini juga dapat menangani UTF32 (UTF8 tidak didukung). Buffer terminal berbasis array yang diketik saat ini ditata untuk UTF32, konversi UTF16 --> UTF32 dilakukan dalam InputHandler.print . Dari sini ada beberapa arah yang mungkin:

  • buat semua UTF16, jadi ubah buffer terminal menjadi UTF16 juga
    Ya masih belum menentukan jalan mana yang harus diambil di sini, tetapi menguji beberapa tata letak buffer dan sampai pada kesimpulan, bahwa nomor 32bit memberikan ruang yang cukup untuk menyimpan charcode + wcwidth + kemungkinan penggabungan overflow (ditangani sangat berbeda), sementara 16bit tidak dapat melakukan itu tanpa mengorbankan bit charcode yang berharga. Perhatikan bahwa kita bahkan dengan buffer UTF16 masih harus melakukan konversi UTF32, karena wcwidth bekerja pada codepoint unicode. Perhatikan juga bahwa buffer berbasis UTF16 akan menghemat lebih banyak memori untuk charcode yang lebih rendah, bahkan lebih tinggi dari charcode bidang BMP akan jarang terjadi. Ini masih perlu penyelidikan.
  • buat pengurai UTF32
    Itu cukup sederhana, cukup ganti semua array yang diketik dengan varian 32bit. Kelemahan - konversi UTF16 ke UTF32 harus dilakukan terlebih dahulu, artinya seluruh input akan dikonversi, bahkan urutan escape yang tidak akan pernah terbentuk dari charcode > 255.
  • buat wcwidth kompatibel dengan UTF16
    Ya jika ternyata UTF16 lebih cocok untuk buffer terminal ini harus dilakukan.

Tentang UTF8: Pengurai saat ini tidak dapat menangani urutan UTF8 asli, terutama karena fakta bahwa karakter perantara berbenturan dengan karakter kontrol C1. Juga UTF8 membutuhkan penanganan aliran yang tepat dengan status perantara tambahan, itu buruk dan tidak boleh ditambahkan ke parser. UTF8 akan lebih baik ditangani sebelumnya, dan mungkin dengan hak konversi ke UTF32 untuk penanganan codepoint yang lebih mudah di semua tempat.

Mengenai kemungkinan pengkodean input UTF8 dan tata letak buffer internal, saya melakukan tes kasar. Untuk mengesampingkan dampak yang jauh lebih tinggi dari perender kanvas pada total runtime, saya melakukannya dengan perender webgl yang akan datang. Dengan patokan ls -lR /usr/lib saya, saya mendapatkan hasil berikut:

  • master + perender webgl saat ini:
    grafik

  • cabang taman bermain, menerapkan #1796, bagian dari #1811 dan penyaji webgl:
    grafik

Cabang taman bermain melakukan konversi awal dari UTF8 ke UTF32 sebelum melakukan penguraian dan penyimpanan (konversi menambahkan ~ 30 ms). Percepatan terutama diperoleh oleh 2 fungsi hot selama aliran input, EscapeSequenceParser.parse (120 md vs. 35 md) dan InputHandler.print (350 md vs. 75 md). Keduanya mendapat banyak manfaat dari sakelar array yang diketik dengan menghemat panggilan .charCodeAt .
Saya juga membandingkan hasil ini dengan array tipe menengah UTF16 - EscapeSequenceParser.parse sedikit lebih cepat (~ 25 ms) tetapi InputHandler.print tertinggal karena pasangan pengganti yang diperlukan dan pencarian codepoint di wcwidth (120 md).
Perhatikan juga bahwa saya sudah pada batas sistem dapat menyediakan data ls (i7 dengan SSD) - percepatan yang diperoleh menambah waktu idle alih-alih membuat proses lebih cepat.

Ringkasan:
Imho penanganan input tercepat yang bisa kita dapatkan adalah campuran transport UTF8 + UTF32 untuk representasi buffer. Sementara transport UTF8 memiliki kecepatan paket byte terbaik untuk input terminal tipikal dan menghilangkan konversi yang tidak masuk akal dari pty melalui beberapa lapisan buffer hingga Terminal.write , buffer berbasis UTF32 dapat menyimpan data dengan cukup cepat. Yang terakhir hadir dengan jejak memori yang sedikit lebih tinggi daripada UTF16 sementara UTF16 sedikit lebih lambat karena penanganan karakter yang lebih rumit dengan lebih banyak tipuan.

Kesimpulan:
Kita harus menggunakan tata letak buffer berbasis UTF32 untuk saat ini. Kami juga harus mempertimbangkan untuk beralih ke pengkodean input UTF8, tetapi ini masih membutuhkan pemikiran lebih lanjut tentang perubahan API dan implikasinya untuk integrator (sepertinya mekanisme ipc elektron tidak dapat menangani data biner tanpa penyandian BASE64 dan pembungkusan JSON, yang akan melawan upaya perf).

Tata letak penyangga untuk dukungan warna sejati yang akan datang:

Saat ini tata letak buffer berbasis array yang diketik adalah sebagai berikut (satu sel):

|    uint32_t    |    uint32_t    |    uint32_t    |
|      attrs     |    codepoint   |     wcwidth    |

di mana attrs berisi semua flag yang dibutuhkan + warna FG dan BG berbasis 9 bit. codepoint menggunakan 21 bit (maks. adalah 0x10FFFF untuk UTF32) + 1 bit untuk menunjukkan penggabungan karakter dan wcwidth 2 bit (berkisar dari 0-2).

Idenya adalah mengatur ulang bit ke paket yang lebih baik untuk memberi ruang bagi nilai RGB tambahan:

  • masukkan wcwidth ke bit tinggi codepoint
  • membagi attrs menjadi grup FG dan BG dengan 32 bit, mendistribusikan flag ke bit yang tidak digunakan
|             uint32_t             |        uint32_t         |        uint32_t         |
|              content             |            FG           |            BG           |
| comb(1) wcwidth(2) codepoint(21) | flags(8) R(8) G(8) B(8) | flags(8) R(8) G(8) B(8) |

Keuntungan dari pendekatan ini adalah akses yang relatif murah ke setiap nilai dengan satu akses indeks dan maks. Operasi 2 bit (dan/atau + shift).

Jejak memori stabil untuk varian saat ini, tetapi masih cukup tinggi dengan 12 byte per sel. Ini dapat lebih dioptimalkan dengan mengorbankan beberapa runtime dengan beralih ke UTF16 dan tipuan attr :

|        uint16_t        |              uint16_t               |
|    BMP codepoint(16)   | comb(1) wcwidth(2) attr pointer(13) |

Sekarang kita turun ke 4 byte per sel + beberapa ruang untuk attrs. Sekarang attrs dapat didaur ulang untuk sel lain juga. Yay misi tercapai! - Eh, sebentar...

Membandingkan jejak memori, pendekatan kedua jelas menang. Tidak demikian untuk runtime, ada tiga faktor utama yang meningkatkan banyak runtime:

  • attr tipuan
    Pointer attr membutuhkan satu pencarian memori tambahan ke dalam wadah data lain.
  • pencocokan attr
    Untuk benar-benar menghemat ruang dengan pendekatan kedua, attr yang diberikan harus dicocokkan dengan attr yang sudah disimpan. Ini adalah tindakan yang rumit, pendekatan langsung dengan hanya melihat melalui semua nilai yang ada dalam O(n) untuk n attrs disimpan, percobaan pohon RB saya berakhir dengan hampir tidak ada manfaat memori saat masih berada di O(log n), dibandingkan dengan akses indeks dalam pendekatan 32 bit dengan O(1). Ditambah pohon memiliki runtime yang lebih buruk untuk beberapa elemen yang disimpan (membayar sekitar> 100 entri dengan impl pohon RB saya).
  • Pasangan pengganti UTF16
    Dengan array yang diketik 16 bit kita harus menurunkan ke UTF16 untuk titik kode, yang juga memperkenalkan hukuman runtime (seperti yang dijelaskan pada komentar di atas). Perhatikan bahwa lebih tinggi dari BMP codepoints hampir tidak terjadi, masih cek sendiri apakah codepoint akan membentuk pasangan pengganti menambahkan ~ 50 ms.

Keseksian pendekatan kedua adalah penghematan memori tambahan. Oleh karena itu saya mengujinya dengan cabang taman bermain (lihat komentar di atas) dengan implementasi BufferLine dimodifikasi:

grafik

Ya, kami agak kembali ke tempat kami memulai sebelum mengubah ke array yang diketik UTF8 + di parser. Penggunaan memori turun dari ~ 1,5 MB menjadi ~ 0,7 MB (aplikasi demo dengan 87 sel dan 1000 baris scrollback).

Dari sini masalah penghematan memori vs kecepatan. Karena kami telah menghemat banyak memori dengan beralih dari array js ke array yang diketik (turun dari ~ 5,6 MB menjadi ~ 1,5 MB untuk tumpukan C++, memotong perilaku Tumpukan JS dan GC yang beracun) saya pikir kita harus pergi ke sini dengan varian yang lebih cepat. Setelah penggunaan memori mendapat masalah mendesak lagi, kami masih dapat beralih ke tata letak buffer yang lebih ringkas seperti yang dijelaskan dalam pendekatan kedua di sini.

Saya setuju, mari kita optimalkan kecepatan selama konsumsi memori tidak menjadi masalah. Saya juga ingin menghindari tipuan sejauh mungkin karena membuat kode lebih sulit untuk dibaca dan dipelihara. Kami sudah memiliki cukup banyak konsep dan penyesuaian dalam basis kode kami yang mempersulit orang (termasuk saya ) untuk mengikuti alur kode - dan membawa lebih banyak dari ini harus selalu dibenarkan dengan alasan yang sangat bagus. IMO menyimpan megabyte memori lain tidak membenarkannya.

Namun demikian, saya sangat menikmati membaca dan belajar dari latihan Anda, terima kasih telah membagikannya dengan sangat detail!

@mofux Ya itu benar - kompleksitas kode jauh lebih tinggi (pengganti UTF16 baca dulu, kals codepoint menengah, wadah pohon dengan ref menghitung entri attr).
Dan karena tata letak 32 bit sebagian besar adalah memori datar (hanya menggabungkan karakter yang membutuhkan tipuan), ada lebih banyak pengoptimalan yang mungkin (juga bagian dari #1811, belum diuji untuk penyaji).

Ada satu keuntungan besar dari mengarahkan ke objek attr: Ini jauh lebih dapat diperluas. Anda dapat menambahkan anotasi, glyph, atau aturan melukis khusus. Anda dapat menyimpan informasi tautan dengan cara yang mungkin lebih bersih dan efisien. Mungkin mendefinisikan antarmuka ICellPainter yang mengetahui cara merender selnya, dan Anda juga dapat menggantungkan properti khusus.

Satu ide adalah menggunakan dua larik per BufferLine: larik Uint32Array, dan ICellPainter, dengan masing-masing satu elemen untuk setiap sel. ICellPainter saat ini adalah properti dari status parser, jadi Anda cukup menggunakan kembali ICellPainter yang sama selama status warna/atribut tidak berubah. Jika Anda perlu menambahkan properti khusus ke sel, pertama-tama Anda mengkloning ICellPainter (jika mungkin dibagikan).

Anda dapat melakukan pra-alokasi ICellPainter untuk kombinasi warna/atribut yang paling umum - setidaknya memiliki objek unik yang sesuai dengan warna/atribut default.

Perubahan gaya (seperti mengubah warna latar depan/latar belakang default) dapat diterapkan hanya dengan memperbarui instance ICellPainter yang sesuai, tanpa harus memperbarui setiap sel.

Ada kemungkinan pengoptimalan: Misalnya, gunakan instance ICellPainter yang berbeda untuk karakter lebar tunggal dan lebar ganda (atau karakter lebar nol). (Itu menghemat 2 bit di setiap elemen Uint32Array.) Ada 11 bit atribut yang tersedia di Uint32Array (lebih banyak jika kita mengoptimalkan karakter BMP). Ini dapat digunakan untuk mengkodekan kombinasi warna/atribut yang paling umum/berguna, yang dapat digunakan untuk mengindeks instance ICellPainter yang paling umum. Jika demikian, array ICellPainter dapat dialokasikan dengan malas - yaitu hanya jika beberapa sel dalam baris memerlukan ICellPainter yang "kurang umum".

Seseorang juga dapat menghapus array _combined untuk karakter non-BMP, dan menyimpannya di ICellPainter. (Itu membutuhkan ICellPainter unik untuk setiap karakter non-BMP, jadi ada tradeoff di sini.)

@PerBothner Ya tipuan lebih fleksibel dan karenanya lebih cocok untuk ekstra yang tidak biasa. Tetapi karena mereka jarang, saya ingin tidak mengoptimalkannya sejak awal.

Beberapa catatan tentang apa yang saya coba di beberapa testbeds:

  • konten string sel
    Berasal dari C++ saya mencoba melihat masalah seperti yang saya lakukan di C++, jadi saya mulai dengan petunjuk untuk konten. Ini adalah pointer string sederhana, tetapi menunjuk sebagian besar waktu ke string char tunggal. Sayang sekali. Oleh karena itu, optimasi pertama saya adalah menyingkirkan abstraksi string dengan langsung menyimpan codepoint alih-alih alamat (jauh lebih mudah di C/C++ daripada di JS). Ini hampir menggandakan akses baca/tulis sambil menyimpan 12 byte per sel (8 byte pointer + 4 byte pada string, 64bit dengan 32bit wchar_t). Sidenote - setengah dari peningkatan kecepatan di sini terkait dengan cache (cache meleset karena lokasi string acak). Ini menjadi jelas dengan solusi saya untuk menggabungkan konten sel - sepotong memori yang saya indeks ketika codepoint memiliki bit gabungan yang disetel (akses lebih cepat di sini karena lokalitas cache yang lebih baik, diuji dengan valgrind). Dibawa ke JS, peningkatan kecepatan tidak terlalu bagus karena konversi string ke angka yang diperlukan (meskipun masih lebih cepat), tetapi penghematan mem bahkan lebih besar (tebak karena beberapa ruang manajemen tambahan untuk tipe JS). Masalahnya adalah StringStorage global untuk hal-hal yang digabungkan dengan manajemen memori eksplisit, antipattern besar di JS. Perbaikan cepat untuk itu adalah objek _combined , yang mendelegasikan pembersihan ke GC. Ini masih dapat diubah dan btw dimaksudkan untuk menyimpan konten string terkait sel sewenang-wenang (melakukan ini dengan mempertimbangkan grafem, tetapi kami tidak akan segera melihatnya karena tidak didukung oleh backend apa pun). Jadi ini adalah tempat untuk menyimpan konten string tambahan berdasarkan sel demi sel.
  • attrs
    Dengan attrs saya mulai "berpikir besar" - dengan global AttributeStorage untuk semua attrs yang pernah digunakan di semua instance terminal (lihat https://github.com/jerch/xterm.js/tree/AttributeStorage). Dari segi memori, ini bekerja dengan cukup baik, terutama karena ppl hanya menggunakan satu set kecil atribut bahkan dengan dukungan warna yang sebenarnya. Performanya tidak begitu baik - terutama karena penghitungan ref (setiap sel harus mengintip ke memori asing ini dua kali) dan attr matching. Dan ketika saya mencoba untuk mengadopsi hal ref ke JS rasanya salah - intinya saya menekan tombol "STOP". Di antara itu ternyata kami telah menghemat banyak memori dan panggilan GC dengan beralih ke array yang diketik, sehingga tata letak memori datar yang sedikit lebih mahal dapat melunasi keunggulan kecepatannya di sini.
    Apa yang saya uji yday (komentar terakhir) adalah array yang diketik kedua pada level baris untuk attrs dengan pohon dari https://github.com/jerch/xterm.js/tree/AttributeStorage untuk pencocokan (hampir seperti ide ICellPainter Anda ). Yah hasilnya tidak menjanjikan, oleh karena itu saya condong ke tata letak 32 bit datar untuk saat ini.

Sekarang tata letak 32 bit datar ini ternyata dioptimalkan untuk hal-hal umum dan tambahan yang tidak biasa tidak dimungkinkan dengannya. Benar. Yah kami masih memiliki penanda (tidak terbiasa dengan mereka jadi saya tidak bisa mengatakan sekarang apa yang mereka mampu), dan ya - masih ada bit gratis di buffer (yang merupakan hal yang baik untuk kebutuhan masa depan, misalnya kita bisa menggunakannya sebagai bendera untuk perlakuan khusus dan semacamnya).

Tbh bagi saya sangat disayangkan bahwa tata letak 16 bit dengan penyimpanan attrs berkinerja buruk, mengurangi separuh penggunaan memori masih merupakan masalah besar (terutama ketika ppl mulai menggunakan garis gulir> 10k), tetapi hukuman runtime dan kompleksitas kode melebihi semakin tinggi mem membutuhkan atm imho.

Bisakah Anda menguraikan ide ICellPainter? Mungkin saya melewatkan beberapa fitur penting sejauh ini.

Tujuan saya untuk DomTerm adalah untuk mengaktifkan dan mendorong interaksi yang lebih kaya seperti yang diaktifkan oleh emulator terminal tradisional. Menggunakan teknologi web memungkinkan banyak hal menarik, jadi sayang jika hanya fokus menjadi emulator terminal tradisional yang cepat. Terutama karena banyak kasus penggunaan untuk xterm.js (seperti REPL untuk IDE) benar-benar dapat mengambil manfaat dari melampaui teks sederhana. Xterm.js bekerja dengan baik di sisi kecepatan (adakah yang mengeluh tentang kecepatan?), tetapi tidak bekerja dengan baik pada fitur (orang - orang mengeluh tentang kehilangan truecolor dan grafik yang disematkan, misalnya). Saya pikir mungkin bermanfaat untuk sedikit lebih fokus pada fleksibilitas dan sedikit lebih sedikit pada kinerja.

_"Bisakah Anda menguraikan ide ICellPainter?"_

Secara umum, ICellPainter merangkum semua data per sel kecuali kode/nilai karakter, yang berasal dari Uint32Array. Itu untuk sel karakter "normal" - untuk gambar yang disematkan dan "kotak" lainnya, kode/nilai karakter mungkin tidak masuk akal.

interface ICellPainter {
    drawOnCanvas(ctx: CanvasRenderingContext2D, code: number, x: number, y: number);
    // transitional - to avoid allocating IGlyphIdentifier we should replace
    //  uses by pair of ICellPainter and code.  Also, a painter may do custom rendering,
    // such that there is no 'code' or IGlyphIdentifier.
    asGlyph(code: number): IGlyphIdentifier;
    width(): number; // in pixels for flexibility?
    height(): number;
    clone(): ICellPainter;
}

Memetakan sel ke ICellPainter dapat dilakukan dengan berbagai cara. Yang jelas adalah untuk setiap BufferLine memiliki array ICellPainter, tetapi itu membutuhkan pointer 8-byte (setidaknya) per sel. Salah satu kemungkinan adalah menggabungkan array _combined dengan array ICellPainter: Jika IS_COMBINED_BIT_MASK diatur, maka ICellPainter juga menyertakan string gabungan. Optimalisasi lain yang mungkin adalah dengan menggunakan bit yang tersedia di Uint32Array sebagai indeks ke dalam array: Itu menambahkan beberapa komplikasi dan tipuan ekstra, tetapi menghemat ruang.

Saya ingin mendorong kami untuk memeriksa apakah kami dapat melakukannya dengan cara monaco-editor melakukannya (saya pikir mereka menemukan cara yang sangat cerdas dan berkinerja). Alih-alih menyimpan informasi tersebut di buffer, mereka memungkinkan Anda untuk membuat decorations . Anda membuat dekorasi untuk rentang baris/kolom dan itu akan menempel pada rentang itu:

// decorations are buffer-dependant (we need to know which buffer to decorate)
const decoration = buffer.createDecoration({
  type: 'link',
  data: 'https://www.google.com',
  range: { startRow: 2, startColumn: 5, endRow: 2, endColumn: 25 }
});

Kemudian penyaji dapat mengambil dekorasi itu dan menggambarnya.

Silakan lihat contoh kecil ini yang menunjukkan bagaimana monaco-editor api terlihat:
https://microsoft.github.io/monaco-editor/playground.html#interacting -with-the-editor-line-and-inline-decorations

Untuk hal-hal seperti merender gambar di dalam terminal monaco menggunakan konsep zona tampilan yang dapat dilihat (di antara konsep lainnya) dalam contoh di sini:
https://microsoft.github.io/monaco-editor/playground.html#interacting -with-the-editor-listening-to-mouse-events

@PerBothner Terima kasih untuk klarifikasi dan sketsanya. Beberapa catatan tentang itu.

Kami akhirnya berencana untuk memindahkan rantai input + buffer ke webworker di masa mendatang. Jadi buffer dimaksudkan untuk beroperasi pada tingkat abstrak dan kami tidak dapat menggunakan hal-hal terkait render/representasi di sana seperti metrik piksel atau node DOM apa pun. Saya melihat kebutuhan Anda untuk ini karena DomTerm sangat dapat disesuaikan, tetapi saya pikir kita harus melakukannya dengan API penanda internal yang ditingkatkan dan dapat belajar di sini dari monaco/vscode (thx for th pointer @mofux).
Saya benar-benar ingin menjaga buffer inti bersih dari hal-hal yang tidak biasa, mungkin kita harus mendiskusikan kemungkinan strategi penanda dengan masalah baru?

Saya masih belum puas dengan hasil dari hasil pengujian layout 16 bit. Karena keputusan akhir belum mendesak (kita tidak akan melihat semua ini sebelum 3.11), saya akan terus mengujinya dengan beberapa perubahan (ini masih merupakan solusi yang lebih menarik bagi saya daripada varian 32 bit).

|             uint32_t             |        uint32_t         |        uint32_t         |
|              content             |            FG           |            BG           |
| comb(1) wcwidth(2) codepoint(21) | flags(8) R(8) G(8) B(8) | flags(8) R(8) G(8) B(8) |

Saya juga berpikir kita harus memulai dengan sesuatu yang dekat dengan ini untuk memulai, kita dapat menjelajahi opsi lain nanti, tetapi ini mungkin yang paling mudah untuk dijalankan. Penipuan atribut pasti memiliki janji IMO karena biasanya tidak banyak atribut yang berbeda dalam sesi terminal.

Saya ingin mendorong kami untuk memeriksa apakah kami dapat melakukannya dengan cara monaco-editor melakukannya (saya pikir mereka menemukan cara yang sangat cerdas dan berkinerja). Alih-alih menyimpan informasi tersebut di buffer, mereka memungkinkan Anda untuk membuat dekorasi. Anda membuat dekorasi untuk rentang baris/kolom dan itu akan menempel pada rentang itu:

Sesuatu seperti ini adalah di mana saya ingin melihat hal-hal pergi. Satu ide yang saya miliki di sepanjang garis ini adalah mengizinkan penyemat untuk melampirkan elemen DOM ke rentang untuk memungkinkan hal-hal khusus digambar. Ada 3 hal yang dapat saya pikirkan saat ini yang ingin saya capai dengan ini:

  • Tautan gambar digarisbawahi dengan cara ini (akan menyederhanakan cara menggambarnya secara signifikan)
  • Izinkan penanda pada baris, seperti * atau sesuatu
  • Izinkan baris untuk "berkedip" untuk menunjukkan sesuatu terjadi

Semua ini dapat dicapai dengan overlay dan ini adalah jenis API yang cukup mudah didekati (mengekspos simpul DOM) dan dapat bekerja terlepas dari jenis penyaji.

Saya tidak yakin kami ingin masuk ke bisnis yang mengizinkan penyemat mengubah cara warna latar belakang dan latar depan digambar.


@jerch Saya akan menempatkan ini pada tonggak 3.11.0 karena saya menganggap masalah ini selesai ketika kami menghapus implementasi array JS yang direncanakan untuk saat itu. https://github.com/xtermjs/xterm.js/pull/1796 juga direncanakan untuk digabungkan saat itu, tetapi masalah ini selalu dimaksudkan untuk meningkatkan tata letak memori buffer.

Juga, banyak dari diskusi nanti ini mungkin akan lebih baik dilakukan di https://github.com/xtermjs/xterm.js/issues/484 dan https://github.com/xtermjs/xterm.js/issues/1852 (dibuat karena tidak ada masalah dekorasi).

@Tyriar Woot - akhirnya ditutup :sweat_smile:

🎉 🕺 🍾

Apakah halaman ini membantu?
0 / 5 - 0 peringkat

Masalah terkait

albinekb picture albinekb  ·  4Komentar

LB-J picture LB-J  ·  3Komentar

Tyriar picture Tyriar  ·  4Komentar

parisk picture parisk  ·  3Komentar

Mlocik97-issues picture Mlocik97-issues  ·  3Komentar