Терпение. Или как один зависший кадр заставил меня построить ещё полсистемы 😉

от автора

Знаете, я хотел сегодня поговорить о довольно глубокой вещи. Зачем нам вообще продолжать что-то делать? Не зачем начинать. Начинать легко. В начале есть идея, азарт, новый репозиторий, первые коммиты и ощущение, что сейчас ты сделаешь что-то чертовски крутое. А потом проходит неделя, месяц, год, и ты сидишь ночью перед монитором и смотришь на картинку, которая почему-то дёргается. Хотя всё должно работать.

Я бы хотел сразу рассказать что я не писарь, Я не пишу и не придумываю идеи с нейросетями, я стараюсь в настроение пару недель готовится и выдавать статью. Я не буду комментировать, но если будут люди которые заметят ошибку, пожалуйста пишите в сообщение как обычно принято на хабре.

С вами Артур Валиев. Сегодня я не буду говорить о смерти, любви, рождении и прочих вещах, куда меня периодически заносит. Сегодня я хочу поговорить о терпении. И да, статья будет длинной. Потерпите.

А ещё хватит уже думать, что каждую мою статью пишет очередной AI. Нет. Я Артур. Пишу сам. Почему я до сих пор сначала пишу черновик в .md — не знаю. Наверное, уже привычка.

Сегодня не будет очередной статьи про «трио». Хотя EvertyDesk и EvertyDisplay сюда всё равно влезут. Потому что внезапно оказалось, что один мой продукт помогает мне отлаживать другой.

Но начнём не с этого.

Когда всё вроде работает

Есть один особенно мерзкий тип багов. Это не когда программа падает. Если она упала — прекрасно. Есть stack trace, есть panic, есть error, есть хоть что-то.

Настоящая мерзость начинается тогда, когда всё работает.

Соединение есть. Картинка идёт. Курсор двигается. Пакеты приезжают. FPS вроде тоже есть. А ты смотришь на экран и понимаешь: что-то не так.

Картинка будто зависает. Не намертво, не на секунды. Просто ощущение, что система постоянно немного опаздывает.

И вот тут начинается любимое развлечение разработчика. Может сеть? Может декодер? Может Windows? Может монитор? Может 30 FPS просто выглядят так? Может relay? Может вообще показалось?

Можно ещё минут двадцать подвигать мышью и окончательно сойти с ума.

Я примерно этим и занимался, пока в какой-то момент не понял: хватит смотреть глазами. Надо измерять.

Где, мать его, пропали миллисекунды

Я перестал спрашивать: «Почему тормозит картинка?» Это слишком большой и практически бесполезный вопрос.

Правильный вопрос оказался другим:

где именно кадр теряет время?

Кадр ведь не возникает на экране магически. У него есть жизнь. Первый фрагмент приехал по сети. Потом остальные. Потом assembler понял, что кадр полный. Потом положил его в очередь. Потом decoder забрал его. Потом распаковал. Потом кадр дошёл до present.

Значит, надо было перестать измерять «FPS» и начать измерять биографию конкретного кадра.

В какой-то момент код начал выглядеть примерно так:

#[derive(Debug, Clone, Default)]struct FrameTrace {    frame_id: u64,    first_fragment_ns: u64,    last_fragment_ns: u64,    assembled_ns: u64,    queue_push_ns: u64,    queue_pop_ns: u64,    decode_begin_ns: u64,    decode_end_ns: u64,    present_submit_ns: u64,    present_done_ns: u64,    prev_decode_done_ns: u64,    fragment_count: u32,    payload_bytes: usize,    is_keyframe: bool,    codec: CodecKind,    transport: TransportKind,}impl FrameTrace {    fn rel_ms(&self, ts: u64) -> f64 {        if ts == 0 || self.first_fragment_ns == 0 {            return 0.0;        }        (ts.saturating_sub(self.first_fragment_ns) as f64)            / 1_000_000.0    }    fn assembly_ms(&self) -> f64 {        self.rel_ms(self.assembled_ns)    }    fn queue_wait_ms(&self) -> f64 {        if self.queue_pop_ns <= self.queue_push_ns {            return 0.0;        }        (self.queue_pop_ns - self.queue_push_ns) as f64            / 1_000_000.0    }    fn decode_ms(&self) -> f64 {        if self.decode_end_ns <= self.decode_begin_ns {            return 0.0;        }        (self.decode_end_ns - self.decode_begin_ns) as f64            / 1_000_000.0    }    fn assembled_frame_waiting_for_decode_ms(&self) -> f64 {        if self.decode_begin_ns <= self.assembled_ns {            return 0.0;        }        (self.decode_begin_ns - self.assembled_ns) as f64            / 1_000_000.0    }    fn decoder_idle_wait_ms(&self) -> f64 {        if self.decode_begin_ns == 0            || self.prev_decode_done_ns == 0            || self.decode_begin_ns <= self.prev_decode_done_ns        {            return 0.0;        }        (self.decode_begin_ns - self.prev_decode_done_ns) as f64            / 1_000_000.0    }}

И это ещё приятная часть.

Потому что дальше надо было протащить timestamps через весь pipeline так, чтобы диагностика сама не превратилась в новую причину тормозов.

И тут внезапно появляется вот такое чудовище:

fn on_video_fragment(    state: &mut VideoRxState,    packet: VideoPacket,    clock: &MonotonicClock,    decode_tx: &Sender<DecodeJob>,    telemetry: &Telemetry,) -> Result<(), VideoError> {    let now = clock.now_ns();    let frame_id = packet.frame_id;    let frame = state        .assembling        .entry(frame_id)        .or_insert_with(|| PendingFrame {            frame_id,            codec: packet.codec,            is_keyframe: packet.is_keyframe,            first_fragment_ns: now,            last_fragment_ns: now,            assembled_ns: 0,            fragment_count: 0,            expected_fragments: packet.fragment_count,            payload_bytes: 0,            fragments: vec![None; packet.fragment_count as usize],        });    if frame.fragment_count == 0 {        frame.first_fragment_ns = now;    }    frame.last_fragment_ns = now;    let idx = packet.fragment_index as usize;    if idx >= frame.fragments.len() {        telemetry.bad_fragment_index.fetch_add(            1,            Ordering::Relaxed,        );        return Err(VideoError::InvalidFragmentIndex {            frame_id,            index: packet.fragment_index,            total: packet.fragment_count,        });    }    if frame.fragments[idx].is_none() {        frame.payload_bytes += packet.payload.len();        frame.fragment_count += 1;        frame.fragments[idx] = Some(packet.payload);    } else {        telemetry.duplicate_fragments.fetch_add(            1,            Ordering::Relaxed,        );    }    if frame.fragment_count != frame.expected_fragments {        return Ok(());    }    let mut completed = state        .assembling        .remove(&frame_id)        .ok_or(VideoError::FrameDisappeared(frame_id))?;    completed.assembled_ns = clock.now_ns();    let mut payload = Vec::with_capacity(completed.payload_bytes);    for fragment in completed.fragments.drain(..) {        let fragment = fragment.ok_or_else(|| {            VideoError::MissingFragmentAfterComplete {                frame_id,            }        })?;        payload.extend_from_slice(&fragment);    }    let queue_push_ns = clock.now_ns();    let job = DecodeJob {        frame_id,        codec: completed.codec,        is_keyframe: completed.is_keyframe,        payload,        trace: FrameTrace {            frame_id,            first_fragment_ns: completed.first_fragment_ns,            last_fragment_ns: completed.last_fragment_ns,            assembled_ns: completed.assembled_ns,            queue_push_ns,            fragment_count: completed.fragment_count,            payload_bytes: completed.payload_bytes,            is_keyframe: completed.is_keyframe,            codec: completed.codec,            transport: state.transport,            ..FrameTrace::default()        },    };    telemetry.frames_assembled.fetch_add(        1,        Ordering::Relaxed,    );    telemetry        .assembled_to_queue_ns        .observe(queue_push_ns - completed.assembled_ns);    match decode_tx.try_send(job) {        Ok(()) => {            telemetry.decode_queue_push.fetch_add(                1,                Ordering::Relaxed,            );        }        Err(TrySendError::Full(job)) => {            telemetry.decode_queue_full.fetch_add(                1,                Ordering::Relaxed,            );            state.on_decode_backpressure(                job.frame_id,                job.is_keyframe,            )?;            return Err(VideoError::DecodeQueueFull {                frame_id,            });        }        Err(TrySendError::Disconnected(_)) => {            return Err(                VideoError::DecoderDisconnected            );        }    }    Ok(())}

Красота.

Вот ради этого мы и изучали программирование.

А теперь выясним, кто именно тормозит

Когда появились timestamps, первая гипотеза была довольно очевидной: может кадр просто поздно приезжает?

Нет.

Кадр собран быстро.

Следующая гипотеза: может decoder иногда простаивает между кадрами и ждёт очередной?

Вот тут уже пришлось смотреть сам worker.

Условно он выглядит так:

fn decoder_worker(    rx: Receiver<DecodeJob>,    presenter: Presenter,    decoder: &mut Decoder,    telemetry: Arc<Telemetry>,    clock: MonotonicClock,) {    let mut prev_decode_done_ns = 0u64;    while let Ok(mut job) = rx.recv() {        let queue_pop_ns = clock.now_ns();        job.trace.queue_pop_ns = queue_pop_ns;        job.trace.prev_decode_done_ns = prev_decode_done_ns;        let decode_begin_ns = clock.now_ns();        job.trace.decode_begin_ns = decode_begin_ns;        if prev_decode_done_ns != 0            && decode_begin_ns > prev_decode_done_ns        {            telemetry.decoder_idle_wait_ns.observe(                decode_begin_ns - prev_decode_done_ns            );        }        if decode_begin_ns > job.trace.assembled_ns {            telemetry                .assembled_frame_waiting_for_decode_ns                .observe(                    decode_begin_ns                        - job.trace.assembled_ns                );        }        let result = decoder.decode(            job.frame_id,            job.codec,            job.is_keyframe,            &job.payload,        );        let decode_end_ns = clock.now_ns();        job.trace.decode_end_ns = decode_end_ns;        prev_decode_done_ns = decode_end_ns;        telemetry.decode_ns.observe(            decode_end_ns - decode_begin_ns        );        match result {            Ok(frame) => {                let present_submit_ns =                    clock.now_ns();                job.trace.present_submit_ns =                    present_submit_ns;                if let Err(err) = presenter.submit(                    frame,                    job.trace.clone(),                ) {                    telemetry.present_errors.fetch_add(                        1,                        Ordering::Relaxed,                    );                    log::warn!(                        "present failed \                         frame={} err={:?}",                        job.frame_id,                        err                    );                }            }            Err(DecodeError::StateMismatch) => {                telemetry                    .decode_state_mismatch                    .fetch_add(                        1,                        Ordering::Relaxed,                    );                log::warn!(                    "decoder state mismatch \                     frame={} keyframe={}",                    job.frame_id,                    job.is_keyframe                );            }            Err(err) => {                telemetry.decode_errors.fetch_add(                    1,                    Ordering::Relaxed,                );                log::warn!(                    "decode failed \                     frame={} err={:?}",                    job.frame_id,                    err                );            }        }        telemetry.frame_traces.push(            job.trace        );    }}

А после этого из системы вываливается примерно такая строка:

frame=71first_fragment          +0.00 mslast_fragment           +1.18 msassembled               +1.24 msqueue_push              +1.26 msprev_decode_done       +15.01 msqueue_pop              +35.17 msdecode_begin           +35.24 msdecode_end             +41.83 mspresent_submit         +41.91 msassembled_wait_decode  34.00 msdecoder_idle_wait       0.00 ms

Вот.

Вот ради этой строки можно было не спать.

Потому что теперь всё очень неприятно, но очень понятно.

Кадр №71 полностью существовал уже примерно через 1,24 мс после первого fragment.

Он был положен в очередь через 1,26 мс.

То есть сеть в данном конкретном случае ни при чём. Frame assembly тоже ни при чём.

Но decode начинается только примерно через 35,24 мс.

Кадр 34 миллисекунды уже существует и просто ждёт.

При этом decoder_idle_wait = 0.

То есть decoder не сидит без работы и не говорит: «Ну где же следующий кадр?»

Нет.

Следующий кадр уже лежит рядом.

Decoder просто ещё занят предыдущим.

Вот это уже проблема.

Но одному кадру верить нельзя

И вот здесь начинается ещё одна ловушка.

Всегда можно найти один страшный кадр.

Один packet loss.

Один scheduler hiccup.

Один Windows moment.

Если я покажу вам один frame с 34 миллисекундами, это ещё вообще ничего не доказывает.

Поэтому пришлось сделать ещё одну прекрасную вещь.

Проверку инвариантов.

Не «посмотрим на график».

А буквально:

система обязана соблюдать определённые отношения между событиями.

И код опять становится красивым:

#[derive(Default)]struct InvariantReport {    checked: usize,    failed: usize,    first_before_last: usize,    last_before_assembled: usize,    assembled_before_queue: usize,    queue_before_decode: usize,    decode_begin_before_end: usize,    decode_end_before_present: usize,    frame_id_monotonic: usize,    negative_wait: usize,    impossible_idle: usize,}fn validate_trace_window(    traces: &[FrameTrace],) -> InvariantReport {    let mut report =        InvariantReport::default();    let mut prev_frame_id = None;    for t in traces {        report.checked += 1;        let mut frame_failed = false;        macro_rules! check {            ($field:ident, $cond:expr) => {                if !($cond) {                    report.$field += 1;                    frame_failed = true;                }            };        }        check!(            first_before_last,            t.first_fragment_ns                <= t.last_fragment_ns        );        check!(            last_before_assembled,            t.last_fragment_ns                <= t.assembled_ns        );        check!(            assembled_before_queue,            t.assembled_ns                <= t.queue_push_ns        );        check!(            queue_before_decode,            t.queue_push_ns                <= t.decode_begin_ns        );        check!(            decode_begin_before_end,            t.decode_begin_ns                <= t.decode_end_ns        );        if t.present_submit_ns != 0 {            check!(                decode_end_before_present,                t.decode_end_ns                    <= t.present_submit_ns            );        }        if let Some(prev) = prev_frame_id {            if t.frame_id <= prev {                report.frame_id_monotonic += 1;                frame_failed = true;            }        }        prev_frame_id = Some(t.frame_id);        if t.decode_begin_ns < t.assembled_ns {            report.negative_wait += 1;            frame_failed = true;        }        if t.prev_decode_done_ns != 0 {            let decoder_idle =                t.decode_begin_ns                    .saturating_sub(                        t.prev_decode_done_ns                    );            let assembled_wait =                t.decode_begin_ns                    .saturating_sub(                        t.assembled_ns                    );            if assembled_wait > 0                && decoder_idle > assembled_wait            {                report.impossible_idle += 1;                frame_failed = true;            }        }        if frame_failed {            report.failed += 1;            log::error!(                "TRACE INVARIANT FAILED: \                 frame={} \                 first={} \                 last={} \                 assembled={} \                 queue={} \                 decode_begin={} \                 decode_end={} \                 present={}",                t.frame_id,                t.first_fragment_ns,                t.last_fragment_ns,                t.assembled_ns,                t.queue_push_ns,                t.decode_begin_ns,                t.decode_end_ns,                t.present_submit_ns,            );        }    }    report}

После этого я беру не один кадр.

Пятьдесят подряд.

И смотрю.

Инварианты — зелёные.

assembled_frame_waiting_for_decode p50 = 34 ms.

decoder_idle_wait p50 = 0 ms.

То есть это уже не артефакт одной странной строки.

Система действительно говорит:

Я собираю кадры быстрее, чем могу протолкнуть их через следующий участок pipeline.

Вот теперь можно чинить.

До этого мы только гадали.

А FPS при этом может врать вам прямо в лицо

И вот здесь одна из причин, почему я перестал слишком сильно уважать FPS как единственную метрику.

Представьте систему, которая честно показывает вам:

30 FPS30 FPS30 FPS30 FPS30 FPS

Красиво.

Стабильно.

Только пользователь двигает окно и чувствует желе.

Почему?

Потому что FPS говорит, сколько кадров вы показали.

Он ничего не говорит о том, сколько каждый конкретный кадр пролежал мёртвым грузом между этапами pipeline.

Можно иметь стабильные 30 FPS и отвратительную latency.

Можно иметь неидеальные 30 FPS и ощущение почти мгновенного управления.

Поэтому в какой-то момент у меня telemetry snapshot начал становиться примерно таким:

struct VideoTelemetrySnapshot {    fps_decode: f64,    fps_present: f64,    rx_mbps: f64,    assemble_p50_ms: f64,    assemble_p95_ms: f64,    assemble_p99_ms: f64,    queue_wait_p50_ms: f64,    queue_wait_p95_ms: f64,    queue_wait_p99_ms: f64,    decode_p50_ms: f64,    decode_p95_ms: f64,    decode_p99_ms: f64,    assembled_wait_decode_p50_ms: f64,    assembled_wait_decode_p95_ms: f64,    assembled_wait_decode_p99_ms: f64,    decoder_idle_p50_ms: f64,    decoder_idle_p95_ms: f64,    decoder_idle_p99_ms: f64,    present_p50_ms: f64,    present_p95_ms: f64,    queue_depth_current: usize,    queue_depth_peak: usize,    frames_received: u64,    frames_assembled: u64,    frames_decoded: u64,    frames_presented: u64,    frames_dropped_before_decode: u64,    frames_dropped_after_decode: u64,    keyframes: u64,    state_mismatch: u64,    requested_keyframes: u64,    transport: TransportKind,    codec: CodecKind,}

А потом ты это печатаешь:

EVRT2 VIDEO PIPELINE-----------------------------------------------------transport                         TCP_RELAYcodec                             EVRTCKfps_decode                        30.01fps_present                       29.98rx                                18.4 Mbpsassemble             p50   1.31 ms                     p95   2.04 ms                     p99   3.11 msqueue_wait           p50  33.87 ms                     p95  38.42 ms                     p99  41.07 msdecode               p50   6.42 ms                     p95   8.10 ms                     p99  10.94 msassembled->decode    p50  34.00 ms                     p95  39.26 ms                     p99  42.18 msdecoder_idle         p50   0.00 ms                     p95   0.00 ms                     p99   0.19 msqueue depth          now   1                     peak  3state mismatch                     0keyframe requests                   0dropped before decode               0dropped after decode                0-----------------------------------------------------

Вот теперь FPS может идти домой.

У нас есть вопросы поважнее.

Иногда баг заставляет написать ещё одну систему

Самое смешное, что ради одного визуального подвисания мне пришлось сделать гораздо больше, чем исправить одну строчку.

Появилась телеметрия. Появились frame id. Появились инварианты. Появилось нормальное понимание очередей. Появилась возможность смотреть не на абстрактные FPS, а на жизнь конкретного кадра.

И вот это уже интереснее самого бага.

Потому что баг уйдёт.

А возможность видеть систему останется.

В следующий раз я уже не буду тыкать палкой в темноту.

У меня будет свет.

И тут внезапно появился EvertyDisplay

Параллельно я делал EvertyDisplay. Вообще другой проект. Виртуальные мониторы. Windows видит их как настоящие дисплеи. У них есть координаты, разрешения, туда можно перетаскивать окна, запускать fullscreen, уводить курсор за границу физического экрана.

Когда я начинал его делать, я вообще не думал: «Отлично, это будет стенд для отладки EvertyDesk».

Но именно так и получилось.

Потому что теперь тест можно делать не в стиле:

Ну давайте подвигав окно туда-сюда посмотрим.

А нормально.

Например, создаём виртуальный монитор слева от основного. Меняем resolution. Перетаскиваем окно через границу. Запускаем движение. Начинаем одновременно смотреть на capture region, dirty map, frame lifecycle и present.

И внезапно EvertyDisplay становится не просто продуктом.

Он становится моим стендом.

Можно даже сделать совершенно мерзкий тест:

for iteration in 0..10_000 {    display.set_mode(DisplayMode {        width: if iteration % 2 == 0 {            1920        } else {            2560        },        height: if iteration % 2 == 0 {            1080        } else {            1440        },        refresh_hz: 60,    })?;    display.set_position(        if iteration % 4 < 2 {            (-1920, 0)        } else {            (1920, 0)        }    )?;    test_window.move_to(        if iteration % 2 == 0 {            display.center()        } else {            primary.center()        }    )?;    test_window.invalidate_full()?;    std::thread::sleep(        Duration::from_millis(16)    );    let snapshot =        evertydesk.telemetry_snapshot();    assert!(        snapshot.queue_depth_current < 4,        "decode queue exploded at iteration {}",        iteration    );    assert!(        snapshot.assembled_wait_decode_p99_ms            < 50.0,        "pipeline latency exploded: {:?}",        snapshot    );}

Это уже не «потыкал мышкой».

Это ты буквально издеваешься над своей системой, пока она не признается, где ей больно.

Вот это я люблю.

«А зачем ты вообще это сделал?»

Мне этот вопрос периодически задают. Зачем свой remote desktop? Зачем свой кодек? Зачем виртуальные мониторы? Зачем ещё один transport? Зачем вообще столько всего?

Не знаю.

Серьёзно. Иногда не знаю.

Потом проходит несколько месяцев, и вдруг оказывается, что решение, которое раньше выглядело как отдельная игрушка, закрывает совершенно другую проблему.

И ты такой:

А.

Вот зачем.

Я вообще перестал верить в идею, что перед каждой строчкой кода обязательно должен быть идеально расписанный бизнес-план на пять лет.

Иногда инженерия — это создание возможностей.

Ты строишь инструмент, а потом обнаруживаешь, где он нужен.

Конечно, если просто писать всё подряд, получится свалка. Но иногда очень полезно позволять себе идти чуть дальше очевидной задачи.

Один bool может уничтожить три дня вашей жизни

Есть ещё одна смешная вещь. Ты можешь решить сто сложных задач: протокол, NAT traversal, relay, свой codec path, prediction, jitter, scheduling, какие-нибудь совершенно безумные race conditions.

А потом тебя на три дня уничтожает один bool.

И ты сидишь и думаешь: серьёзно? Я всё это написал, чтобы сейчас проиграть флагу?

Да.

Именно так.

У меня уже были вещи уровня:

match command {    HostCommand::Start => {        running.store(            true,            Ordering::Release        );        start_host()?;    }    HostCommand::Stop => {        stop_host()?;        // угадайте, чего здесь когда-то        // могло не хватать    }}

И где-то дальше:

while running.load(Ordering::Acquire) {    match listener.accept() {        Ok(stream) => {            spawn_session(stream);        }        Err(err)            if err.kind()                == std::io::ErrorKind::WouldBlock =>        {            std::thread::sleep(                Duration::from_millis(5)            );        }        Err(err) => {            return Err(err.into());        }    }}

А потом ты три часа смотришь, почему Stop как будто работает, а listener всё ещё жив.

Потом ещё час.

Потом идёшь пить кофе.

Возвращаешься.

Смотришь.

HostCommand::Stop => {    running.store(        false,        Ordering::Release    );    stop_host()?;}

Спасибо.

До свидания.

Багу вообще плевать на твои предыдущие достижения.

Он не становится проще от того, что вчера ты написал что-то умное.

Раньше меня это бесило. Если я три дня не мог найти проблему, внутри сразу начиналось: может, я вообще не умею программировать?

Сейчас я смотрю проще.

Баг — это просто то место, где система знает больше тебя.

Она уже знает, почему работает именно так.

Ты пока нет.

Вот и всё.

Отладка — это расследование

Я всё чаще отношусь к debugging именно так. Не как к борьбе. Как к расследованию.

Сеть говорит: «Я всё отправила».

Assembler: «Я всё собрал».

Queue: «Кадр давно лежит у меня».

Decoder сидит в углу и молчит.

Хорошо.

Давайте timestamps.

Ага.

Попался.

Мне нравится такой подход. Без магии. Без «мне кажется». Без «давай увеличим buffer и посмотрим».

Хотя, конечно, иногда мы всё равно увеличиваем buffer и смотрим.

Мы же программисты, а не святые.

Но в какой-то момент надо перестать гадать.

Терпение — это вообще не про ожидание

Вот здесь мы наконец дошли до того, зачем я вообще начал эту статью.

Я долго неправильно понимал слово «терпение». Мне казалось, терпение — это когда ты просто долго выдерживаешь. Тебе плохо, сложно, ничего не получается, а ты такой сидишь и терпишь.

Нет.

Это бесполезное терпение.

В разработке терпение выглядит иначе.

Не получилось воспроизвести баг — строишь сценарий воспроизведения. Не понимаешь, где задержка — добавляешь измерения. Логи превратились в помойку — делаешь метрику. Метрика врёт — строишь инварианты. Проверил пять подсистем и ни одна не виновата — отлично. Теперь у тебя на пять подозреваемых меньше.

Это тоже результат.

Иногда за день ты вообще ничего не исправил. Но вечером уже точно знаешь: проблема не здесь, не здесь и не здесь.

И это хороший день.

Особенно когда ты один

Когда работаешь один, иногда становится особенно смешно.

Нельзя взять тикет и написать: «Передаём в networking team».

Networking team — это ты. Video team — ты. Desktop team — опять ты. QA — угадайте кто. Пользователь, который в три часа ночи двигает мышью и говорит «что-то лагает», тоже ты.

С одной стороны, это иногда задолбывает. С другой — ты начинаешь видеть систему целиком.

Не отдельный класс. Не отдельную функцию.

А путь от захвата пикселя до того момента, когда он появляется на другом экране.

Мне это всегда было интересно.

Большие системы строятся очень тупо

Со стороны всё иногда выглядит так, будто у меня был какой-то великий план. EvertyDesk. EVRTCK. EvertyDisplay. Свой transport. Свои эксперименты с prediction. Потом ещё что-нибудь.

На самом деле всё гораздо тупее.

Я просто каждый раз решал следующую проблему.

Нужен remote desktop. Хорошо.

Не устраивает передача изображения. Хорошо. Пробуем своё.

Не устраивает latency. Хорошо. Ищем где.

Не хватает инструментов для тестирования мониторов. Хорошо. Теперь у меня виртуальные мониторы.

Вот так и появляется система.

Не из: «Сегодня я построю уникальную архитектуру».

А из:

«Почему этот чёртов кадр лежит в очереди 34 миллисекунды?»

А потом через год ты смотришь на репозиторий и понимаешь:

О.

Тут уже полсамолёта.

Мы постоянно ждём момента, когда всё закончится

Мне кажется, это вообще большая ловушка.

Вот сейчас допишу. Вот сейчас выпущу. Вот сейчас исправлю последние баги. Вот сейчас доведу до идеала.

И потом станет спокойно.

Не станет.

EvertyDesk никогда не будет полностью готов. EVRTCK тоже. EvertyDisplay тоже.

Потому что как только ты исправляешь одну проблему, ты начинаешь видеть следующую.

Когда у тебя latency 100 мс, ты хочешь 50. Получил 50 — начинаешь ненавидеть 30. Получил 15 — внезапно уже видишь 5.

И конца нет.

Раньше меня это напрягало.

Сейчас мне кажется, именно в этом и есть смысл.

Если ты начал видеть проблемы, которые год назад даже не замечал, — значит, ты вырос.

Старый код должен немного бесить

Иногда открываешь свой код двухлетней давности. Смотришь и думаешь: кто написал эту хрень?

Потом открываешь git blame.

А.

Ну конечно.

Я.

И это прекрасно.

Если старый код кажется тебе идеальным, возможно, ты просто никуда не ушёл.

Мне нравится видеть свои старые ошибки. Не потому что я люблю плохой код. А потому что сегодняшний я уже понимает, почему он плохой.

Два года назад не понимал.

Сейчас понимаю.

Значит, что-то произошло.

Моё определение терпения

Я не хочу превращать это в красивую мотивационную концовку. Не люблю такое.

Поэтому скажу проще.

Для меня терпение сейчас — это не «когда-нибудь всё обязательно станет хорошо».

Я вообще не знаю, станет или нет.

Терпение — это когда сегодня не получилось, а завтра ты открываешь проект ещё раз.

Не потому что ты великий.

Не потому что железная воля.

А потому что вчера у тебя было пять гипотез, сегодня осталось три. Потом две. Потом одна.

И в какой-то момент огромная непонятная проблема превращается в конкретную вещь.

Очередь.

Состояние.

Пакет.

Таймер.

Один bool.

Один кадр.

Ты исправляешь. Компилируешь. Запускаешь. Двигаешь мышь.

Картинка идёт плавно.

Сидишь секунд десять.

Радуешься.

А потом замечаешь следующий баг.

Ну конечно.

Открываешь код.

Идём дальше.

Артур Валиев ≈ V

ссылка на оригинал статьи https://habr.com/ru/articles/1085912/