crates: improved json codec decoding performance
What changed, and why it matters
This commit is a straightforward performance optimization for the JSON message decoder used in Core Lightning's Rust components. It replaces a separator-search helper that scanned from the beginning of the buffer every time with a decoder that remembers where it left off, so it does not re-examine bytes it already checked. There is no security-relevant change here: the message-splitting rule (two consecutive newlines) and the UTF-8 validation remain exactly the same.
No security action needed. Treat as a normal performance refactor and review through standard code-review channels.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch refactors MultiLineCodec in cln-rpc/src/codec.rs and plugins/src/codec.rs. It removes the find_separator helper (which used buf.iter().zip(buf.iter().skip(1)).position(…)) and adds a search_pos field to the codec state. The decode implementation now resumes scanning from search_pos instead of from index 0, and updates search_pos to bytes.len().saturating_sub(1) when no separator is found. The split_to/utf8 logic is unchanged. The removed unit tests only covered the deleted helper; the existing MultiLineCodec tests remain.
Changed components
cln-rpc/src/codec.rsplugins/src/codec.rsInspect captured patch +41 / −68
diff --git a/cln-rpc/src/codec.rs b/cln-rpc/src/codec.rs
index 25af7025..ccecc734 100644
--- a/cln-rpc/src/codec.rs
+++ b/cln-rpc/src/codec.rs
@@ -12,22 +12,13 @@ use std::{io, str};
use tokio_util::codec::{Decoder, Encoder};
pub use crate::jsonrpc::JsonRpc;
-use crate::{
- model::{Request},
- notifications::Notification,
-};
+use crate::{model::Request, notifications::Notification};
/// A simple codec that parses messages separated by two successive
/// `\n` newlines.
#[derive(Default)]
-pub struct MultiLineCodec {}
-
-/// Find two consecutive newlines, i.e., an empty line, signalling the
-/// end of one message and the start of the next message.
-fn find_separator(buf: &mut BytesMut) -> Option<usize> {
- buf.iter()
- .zip(buf.iter().skip(1))
- .position(|b| *b.0 == b'\n' && *b.1 == b'\n')
+pub struct MultiLineCodec {
+ search_pos: usize,
}
fn utf8(buf: &[u8]) -> Result<&str, io::Error> {
@@ -39,14 +30,24 @@ impl Decoder for MultiLineCodec {
type Item = String;
type Error = Error;
fn decode(&mut self, buf: &mut BytesMut) -> Result<Option<Self::Item>, Error> {
- if let Some(newline_offset) = find_separator(buf) {
- let line = buf.split_to(newline_offset + 2);
- let line = &line[..line.len() - 2];
- let line = utf8(line)?;
- Ok(Some(line.to_string()))
- } else {
- Ok(None)
+ let bytes = &buf[..];
+ let mut i = self.search_pos;
+
+ while i + 1 < bytes.len() {
+ if bytes[i] == b'\n' && bytes[i + 1] == b'\n' {
+ let line = buf.split_to(i + 2);
+ let line = &line[..line.len() - 2];
+
+ self.search_pos = 0;
+
+ return Ok(Some(utf8(line)?.to_owned()));
+ }
+ i += 1;
}
+
+ self.search_pos = bytes.len().saturating_sub(1);
+
+ Ok(None)
}
}
@@ -129,27 +130,11 @@ impl Decoder for JsonRpcCodec {
#[cfg(test)]
mod test {
- use super::{find_separator, JsonCodec, MultiLineCodec};
+ use super::{JsonCodec, MultiLineCodec};
use bytes::{BufMut, BytesMut};
use serde_json::json;
use tokio_util::codec::{Decoder, Encoder};
- #[test]
- fn test_separator() {
- struct Test(String, Option<usize>);
- let tests = vec![
- Test("".to_string(), None),
- Test("}\n\n".to_string(), Some(1)),
- Test("\"hello\"},\n\"world\"}\n\n".to_string(), Some(18)),
- ];
-
- for t in tests.iter() {
- let mut buf = BytesMut::new();
- buf.put_slice(t.0.as_bytes());
- assert_eq!(find_separator(&mut buf), t.1);
- }
- }
-
#[test]
fn test_ml_decoder() {
struct Test(String, Option<String>, String);
diff --git a/plugins/src/codec.rs b/plugins/src/codec.rs
index ad512872..75068e7e 100644
--- a/plugins/src/codec.rs
+++ b/plugins/src/codec.rs
@@ -17,14 +17,8 @@ use crate::messages::{Notification, Request};
/// A simple codec that parses messages separated by two successive
/// `\n` newlines.
#[derive(Default)]
-pub struct MultiLineCodec {}
-
-/// Find two consecutive newlines, i.e., an empty line, signalling the
-/// end of one message and the start of the next message.
-fn find_separator(buf: &mut BytesMut) -> Option<usize> {
- buf.iter()
- .zip(buf.iter().skip(1))
- .position(|b| *b.0 == b'\n' && *b.1 == b'\n')
+pub struct MultiLineCodec {
+ search_pos: usize,
}
fn utf8(buf: &[u8]) -> Result<&str, io::Error> {
@@ -36,14 +30,24 @@ impl Decoder for MultiLineCodec {
type Item = String;
type Error = Error;
fn decode(&mut self, buf: &mut BytesMut) -> Result<Option<Self::Item>, Error> {
- if let Some(newline_offset) = find_separator(buf) {
- let line = buf.split_to(newline_offset + 2);
- let line = &line[..line.len() - 2];
- let line = utf8(line)?;
- Ok(Some(line.to_string()))
- } else {
- Ok(None)
+ let bytes = &buf[..];
+ let mut i = self.search_pos;
+
+ while i + 1 < bytes.len() {
+ if bytes[i] == b'\n' && bytes[i + 1] == b'\n' {
+ let line = buf.split_to(i + 2);
+ let line = &line[..line.len() - 2];
+
+ self.search_pos = 0;
+
+ return Ok(Some(utf8(line)?.to_owned()));
+ }
+ i += 1;
}
+
+ self.search_pos = bytes.len().saturating_sub(1);
+
+ Ok(None)
}
}
@@ -125,27 +129,11 @@ impl Decoder for JsonRpcCodec {
#[cfg(test)]
mod test {
- use super::{find_separator, JsonCodec, MultiLineCodec};
+ use super::{JsonCodec, MultiLineCodec};
use bytes::{BufMut, BytesMut};
use serde_json::json;
use tokio_util::codec::{Decoder, Encoder};
- #[test]
- fn test_separator() {
- struct Test(String, Option<usize>);
- let tests = vec![
- Test("".to_string(), None),
- Test("}\n\n".to_string(), Some(1)),
- Test("\"hello\"},\n\"world\"}\n\n".to_string(), Some(18)),
- ];
-
- for t in tests.iter() {
- let mut buf = BytesMut::new();
- buf.put_slice(t.0.as_bytes());
- assert_eq!(find_separator(&mut buf), t.1);
- }
- }
-
#[test]
fn test_ml_decoder() {
struct Test(String, Option<String>, String);
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.