This is an automated email from the ASF dual-hosted git repository.

Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git


The following commit(s) were added to refs/heads/master by this push:
     new 14018c416f perf(admin): reuse canonical websocket reconciliation 
snapshots. (#7842)
14018c416f is described below

commit 14018c416fcf43544083933d7264b2d84c112eb3
Author: redyyang <[email protected]>
AuthorDate: Tue Oct 20 12:38:08 2026 +0800

    perf(admin): reuse canonical websocket reconciliation snapshots. (#7842)
---
 docs/benchmarks/websocket-reconciliation/run-1.csv | 145 +++++++++
 docs/benchmarks/websocket-reconciliation/run-2.csv | 145 +++++++++
 docs/websocket-reconciliation-benchmark.md         | 226 ++++++++++++++
 .../websocket/WebsocketDataReconciler.java         |  54 +++-
 .../WebsocketDataReconcilerBenchmark.java          | 323 +++++++++++++++++++++
 .../websocket/WebsocketDataReconcilerTest.java     | 165 +++++++++++
 6 files changed, 1046 insertions(+), 12 deletions(-)

diff --git a/docs/benchmarks/websocket-reconciliation/run-1.csv 
b/docs/benchmarks/websocket-reconciliation/run-1.csv
new file mode 100644
index 0000000000..c6c38c92af
--- /dev/null
+++ b/docs/benchmarks/websocket-reconciliation/run-1.csv
@@ -0,0 +1,145 @@
+variant,group,rows,handle_chars,changed,sample,iterations,wall_ns_per_tick,cpu_ns_per_tick,allocated_bytes_per_tick,peak_observed_heap_bytes,gc_count,gc_ms,load_calls_per_tick,publishes_per_tick,payload_chars
+baseline,PLUGIN,1000,256,false,0,5,1969300,3125000,2463062,32705912,0,0,3,0,376999
+canonical,PLUGIN,1000,256,false,0,5,1747720,3125000,2089040,31660216,0,0,3,0,376999
+canonical,PLUGIN,1000,256,false,1,5,2094900,3125000,2121040,31661288,0,0,3,0,376999
+baseline,PLUGIN,1000,256,false,1,5,2199800,3125000,2491280,32709992,0,0,3,0,376999
+baseline,PLUGIN,1000,256,false,2,5,1790520,0,2463306,32710120,0,0,3,0,376999
+canonical,PLUGIN,1000,256,false,2,5,1657460,0,2089040,31661664,0,0,3,0,376999
+baseline,PLUGIN,1000,256,true,0,5,4009380,6250000,9235152,68386456,0,0,3,1,376994
+canonical,PLUGIN,1000,256,true,0,5,1466220,3125000,3342936,36929296,0,0,3,1,376994
+canonical,PLUGIN,1000,256,true,1,5,1486420,0,3342936,36929440,0,0,3,1,376994
+baseline,PLUGIN,1000,256,true,1,5,3817740,3125000,9235152,68386952,0,0,3,1,376994
+baseline,PLUGIN,1000,256,true,2,5,3908080,3125000,9235152,68387104,0,0,3,1,376994
+canonical,PLUGIN,1000,256,true,2,5,1610440,3125000,3342936,36930832,0,0,3,1,376994
+baseline,SELECTOR,1000,256,false,0,5,1954400,3125000,3505184,44890952,0,0,3,0,552001
+canonical,SELECTOR,1000,256,false,0,5,1951140,3125000,2961040,37551048,0,0,3,0,552001
+canonical,SELECTOR,1000,256,false,1,5,2087080,3125000,2961040,37551192,0,0,3,0,552001
+baseline,SELECTOR,1000,256,false,1,5,1942760,3125000,3505184,44891376,0,0,3,0,552001
+baseline,SELECTOR,1000,256,false,2,5,1958760,3125000,3505184,44891504,0,0,3,0,552001
+canonical,SELECTOR,1000,256,false,2,5,2239140,3125000,2961040,37551600,0,0,3,0,552001
+baseline,SELECTOR,1000,256,true,0,5,6179380,6250000,12620845,95223560,0,0,3,1,551994
+canonical,SELECTOR,1000,256,true,0,5,2101800,3125000,4752240,50135104,0,0,3,1,551994
+canonical,SELECTOR,1000,256,true,1,5,2006180,3125000,4752240,50135248,0,0,3,1,551994
+baseline,SELECTOR,1000,256,true,1,5,5863020,6250000,12620976,95224144,0,0,3,1,551994
+baseline,SELECTOR,1000,256,true,2,5,5652220,3125000,12620808,95225160,0,0,3,1,551994
+canonical,SELECTOR,1000,256,true,2,5,2020700,3125000,4752240,50136576,0,0,3,1,551994
+baseline,RULE,1000,256,false,0,5,2438120,3125000,3455104,44874936,0,0,3,0,546997
+canonical,RULE,1000,256,false,0,5,2614300,3125000,2920880,37535024,0,0,3,0,546997
+canonical,RULE,1000,256,false,1,5,2893220,3125000,2920880,37535168,0,0,3,0,546997
+baseline,RULE,1000,256,false,1,5,2334720,0,3455104,44875504,0,0,3,0,546997
+baseline,RULE,1000,256,false,2,5,2310980,0,3455104,44875448,0,0,3,0,546997
+canonical,RULE,1000,256,false,2,5,2758440,3125000,2920880,37535536,0,0,3,0,546997
+baseline,RULE,1000,256,true,0,5,8824940,9375000,12550480,94159184,0,0,3,1,546994
+canonical,RULE,1000,256,true,0,5,3000280,3125000,4688624,51167688,0,0,3,1,546994
+canonical,RULE,1000,256,true,1,5,2745020,3125000,4688624,51167824,0,0,3,1,546994
+baseline,RULE,1000,256,true,1,5,6874000,6250000,12550480,94159624,0,0,3,1,546994
+baseline,RULE,1000,256,true,2,5,5677360,6250000,12550480,94159744,0,0,3,1,546994
+canonical,RULE,1000,256,true,2,5,2986000,3125000,4688624,51168248,0,0,3,1,546994
+baseline,PLUGIN,1000,4096,false,0,5,11623680,9375000,25499184,168344576,0,0,3,0,4216999
+canonical,PLUGIN,1000,4096,false,0,5,12457260,12500000,21289040,137936000,0,0,3,0,4216999
+canonical,PLUGIN,1000,4096,false,1,5,13357920,15625000,21289040,137936152,0,0,3,0,4216999
+baseline,PLUGIN,1000,4096,false,1,5,12736800,12500000,25499184,168344992,0,0,3,0,4216999
+baseline,PLUGIN,1000,4096,false,2,5,13141340,12500000,25499184,168345120,0,0,3,0,4216999
+canonical,PLUGIN,1000,4096,false,2,5,13339820,12500000,21289040,137936544,0,0,3,0,4216999
+baseline,PLUGIN,1000,4096,true,0,5,38477100,37500000,61804064,373867712,0,0,3,1,4216994
+canonical,PLUGIN,1000,4096,true,0,5,12807320,12500000,34243384,223921472,0,0,3,1,4216994
+canonical,PLUGIN,1000,4096,true,1,5,13358280,12500000,34243384,223921616,0,0,3,1,4216994
+baseline,PLUGIN,1000,4096,true,1,5,39062960,40625000,61804064,373868112,0,0,3,1,4216994
+baseline,PLUGIN,1000,4096,true,2,5,38984220,40625000,61804064,373868240,0,0,3,1,4216994
+canonical,PLUGIN,1000,4096,true,2,5,13084440,15625000,34243384,223922000,0,0,3,1,4216994
+baseline,SELECTOR,1000,4096,false,0,5,13376300,12500000,26545184,171797528,0,0,3,0,4392001
+canonical,SELECTOR,1000,4096,false,0,5,10793160,9375000,22161040,142437632,0,0,3,0,4392001
+canonical,SELECTOR,1000,4096,false,1,5,13289220,12500000,22161040,142437784,0,0,3,0,4392001
+baseline,SELECTOR,1000,4096,false,1,5,13204600,12500000,26545184,172846624,0,0,3,0,4392001
+baseline,SELECTOR,1000,4096,false,2,5,13999560,15625000,26545184,171798184,0,0,3,0,4392001
+canonical,SELECTOR,1000,4096,false,2,5,13015800,12500000,22161040,142438192,0,0,3,0,4392001
+baseline,SELECTOR,1000,4096,true,0,5,44319440,43750000,73503512,432893976,0,0,3,1,4391994
+canonical,SELECTOR,1000,4096,true,0,5,13998160,15625000,35652688,227373216,0,0,3,1,4391994
+canonical,SELECTOR,1000,4096,true,1,5,15093100,15625000,35652688,227373368,0,0,3,1,4391994
+baseline,SELECTOR,1000,4096,true,1,5,45607540,46875000,73503512,432894400,0,0,3,1,4391994
+baseline,SELECTOR,1000,4096,true,2,5,42114940,40625000,73503512,433943112,0,0,3,1,4391994
+canonical,SELECTOR,1000,4096,true,2,5,12576220,12500000,35652688,228422352,0,0,3,1,4391994
+baseline,RULE,1000,4096,false,0,5,14039880,15625000,26495104,171768248,0,0,3,0,4386997
+canonical,RULE,1000,4096,false,0,5,14003020,15625000,22120880,142408248,0,0,3,0,4386997
+canonical,RULE,1000,4096,false,1,5,14152540,15625000,22120880,142408392,0,0,3,0,4386997
+baseline,RULE,1000,4096,false,1,5,14017540,15625000,26495104,172819096,0,0,3,0,4386997
+baseline,RULE,1000,4096,false,2,5,13997240,15625000,26495104,171770584,0,0,3,0,4386997
+canonical,RULE,1000,4096,false,2,5,13265320,12500000,22120880,142410584,0,0,3,0,4386997
+baseline,RULE,1000,4096,true,0,5,46003780,43750000,73043280,417408560,3,3,3,1,4386994
+canonical,RULE,1000,4096,true,0,5,15659840,12500000,35589104,210354584,3,2,3,1,4386994
+canonical,RULE,1000,4096,true,1,5,16027480,15625000,35589104,211400904,3,2,3,1,4386994
+baseline,RULE,1000,4096,true,1,5,45804300,43750000,73043280,417413368,3,4,3,1,4386994
+baseline,RULE,1000,4096,true,2,5,48158700,46875000,73043280,417409152,3,2,3,1,4386994
+canonical,RULE,1000,4096,true,2,5,14179300,12500000,35589104,210346576,3,2,3,1,4386994
+baseline,PLUGIN,10000,256,false,0,5,23160700,18750000,24744968,147082088,3,4,3,0,3778999
+canonical,PLUGIN,10000,256,false,0,5,16302740,15625000,21047880,136839896,0,0,3,0,3778999
+canonical,PLUGIN,10000,256,false,1,5,13324620,12500000,21047880,136840048,0,0,3,0,3778999
+baseline,PLUGIN,10000,256,false,1,5,19926800,15625000,24744968,146947736,3,2,3,0,3778999
+baseline,PLUGIN,10000,256,false,2,5,19663140,15625000,24744968,146966744,3,3,3,0,3778999
+canonical,PLUGIN,10000,256,false,2,5,19569280,15625000,21047880,136840440,0,0,3,0,3778999
+baseline,PLUGIN,10000,256,true,0,5,59989860,62500000,91005208,493929368,3,3,3,1,3778994
+canonical,PLUGIN,10000,256,true,0,5,23129240,25000000,38851952,218353512,3,2,3,1,3778994
+canonical,PLUGIN,10000,256,true,1,5,20206060,21875000,38851952,218063336,3,2,3,1,3778994
+baseline,PLUGIN,10000,256,true,1,5,63425420,59375000,91005208,494913664,3,3,3,1,3778994
+baseline,PLUGIN,10000,256,true,2,5,66565440,62500000,91005208,494967128,3,3,3,1,3778994
+canonical,PLUGIN,10000,256,true,2,5,22016480,21875000,38851952,218427288,3,2,3,1,3778994
+baseline,SELECTOR,10000,256,false,0,5,30994140,31250000,35132872,205521888,3,2,3,0,5529001
+canonical,SELECTOR,10000,256,false,0,5,32280420,34375000,29623784,187123112,0,0,3,0,5529001
+canonical,SELECTOR,10000,256,false,1,5,32968340,34375000,29623784,188171840,0,0,3,0,5529001
+baseline,SELECTOR,10000,256,false,1,5,32070960,31250000,35132872,205612376,3,4,3,0,5529001
+baseline,SELECTOR,10000,256,false,2,5,32875520,28125000,35132872,205290440,3,2,3,0,5529001
+canonical,SELECTOR,10000,256,false,2,5,30719180,31250000,29623784,187123736,0,0,3,0,5529001
+baseline,SELECTOR,10000,256,true,0,5,93112860,90625000,133386200,579550432,4,4,3,1,5528994
+canonical,SELECTOR,10000,256,true,0,5,35968740,34375000,54977632,308581856,3,3,3,1,5528994
+canonical,SELECTOR,10000,256,true,1,5,34034680,34375000,54977632,308747704,3,2,3,1,5528994
+baseline,SELECTOR,10000,256,true,1,5,91821040,87500000,133386200,720061632,3,2,3,1,5528994
+baseline,SELECTOR,10000,256,true,2,5,90481320,90625000,133386200,720262840,3,3,3,1,5528994
+canonical,SELECTOR,10000,256,true,2,5,34902360,31250000,54977632,308638192,3,3,3,1,5528994
+baseline,RULE,10000,256,false,0,5,33248020,31250000,34632792,203496592,3,2,3,0,5478997
+canonical,RULE,10000,256,false,0,5,29500520,28125000,29223624,184695408,0,0,3,0,5478997
+canonical,RULE,10000,256,false,1,5,27540540,28125000,29223624,184695552,0,0,3,0,5478997
+baseline,RULE,10000,256,false,1,5,31106500,31250000,34632792,203003168,3,2,3,0,5478997
+baseline,RULE,10000,256,false,2,5,31060600,31250000,34632792,202591520,3,4,3,0,5478997
+canonical,RULE,10000,256,false,2,5,27260100,28125000,29223624,184696016,0,0,3,0,5478997
+baseline,RULE,10000,256,true,0,5,74649560,75000000,122493488,652335376,3,2,3,1,5478994
+canonical,RULE,10000,256,true,0,5,29754740,31250000,54232552,306195872,3,2,3,1,5478994
+canonical,RULE,10000,256,true,1,5,27061340,28125000,54232552,306340040,3,2,3,1,5478994
+baseline,RULE,10000,256,true,1,5,73205240,68750000,122493488,651730056,3,3,3,1,5478994
+baseline,RULE,10000,256,true,2,5,73266980,68750000,122493488,650674120,3,2,3,1,5478994
+canonical,RULE,10000,256,true,2,5,27252580,25000000,54232552,306340424,3,3,3,1,5478994
+baseline,PLUGIN,10000,4096,false,0,5,122196320,121875000,255144872,789073920,8,12,3,0,42178999
+canonical,PLUGIN,10000,4096,false,0,5,122491400,118750000,213047784,568456808,2,4,3,0,42178999
+canonical,PLUGIN,10000,4096,false,1,5,130893780,131250000,213047784,589428480,4,6,3,0,42178999
+baseline,PLUGIN,10000,4096,false,1,5,124003440,118750000,255144872,791279464,8,13,3,0,42178999
+baseline,PLUGIN,10000,4096,false,2,5,116041140,115625000,255144872,723646480,5,10,3,0,42178999
+canonical,PLUGIN,10000,4096,false,2,5,114958120,115625000,213047784,571923280,2,5,3,0,42178999
+baseline,PLUGIN,10000,4096,true,0,5,341965640,318750000,669390360,786636088,25,93,3,1,42178994
+canonical,PLUGIN,10000,4096,true,0,5,132823560,121875000,395077136,788779656,14,25,3,1,42178994
+canonical,PLUGIN,10000,4096,true,1,5,128694980,128125000,395077136,794873000,13,19,3,1,42178994
+baseline,PLUGIN,10000,4096,true,1,5,360866020,346875000,669390360,796998360,26,83,3,1,42178994
+baseline,PLUGIN,10000,4096,true,2,5,368805240,350000000,669390360,802586568,25,102,3,1,42178994
+canonical,PLUGIN,10000,4096,true,2,5,145602540,134375000,395077136,788244336,14,27,3,1,42178994
+baseline,SELECTOR,10000,4096,false,0,5,136362440,131250000,265532872,793667800,8,13,3,0,43929001
+canonical,SELECTOR,10000,4096,false,0,5,115596240,115625000,221623784,557912224,2,3,3,0,43929001
+canonical,SELECTOR,10000,4096,false,1,5,115722940,109375000,221623784,568398136,4,4,3,0,43929001
+baseline,SELECTOR,10000,4096,false,1,5,124784900,121875000,265532872,772232240,8,14,3,0,43929001
+baseline,SELECTOR,10000,4096,false,2,5,121111380,121875000,265532872,734073416,5,10,3,0,43929001
+canonical,SELECTOR,10000,4096,false,2,5,125265680,121875000,221623784,587110752,2,3,3,0,43929001
+baseline,SELECTOR,10000,4096,true,0,5,379077480,356250000,700524056,781123816,25,45,3,1,43928994
+canonical,SELECTOR,10000,4096,true,0,5,154139500,146875000,411202912,767438584,14,26,3,1,43928994
+canonical,SELECTOR,10000,4096,true,1,5,149744100,134375000,411202912,768650272,13,26,3,1,43928994
+baseline,SELECTOR,10000,4096,true,1,5,390614700,362500000,700524056,801374904,24,110,3,1,43928994
+baseline,SELECTOR,10000,4096,true,2,5,383247140,359375000,700524056,798785088,25,104,3,1,43928994
+canonical,SELECTOR,10000,4096,true,2,5,158194580,143750000,411202912,800645472,16,33,3,1,43928994
+baseline,RULE,10000,4096,false,0,5,119412660,115625000,265032792,772954816,8,11,3,0,43878997
+canonical,RULE,10000,4096,false,0,5,114394620,115625000,221223624,556503120,2,2,3,0,43878997
+canonical,RULE,10000,4096,false,1,5,115048860,115625000,221223624,579571944,4,5,3,0,43878997
+baseline,RULE,10000,4096,false,1,5,118940620,112500000,265032792,769820424,8,12,3,0,43878997
+baseline,RULE,10000,4096,false,2,5,119684320,112500000,265032792,733556232,5,11,3,0,43878997
+canonical,RULE,10000,4096,false,2,5,113438140,115625000,221223624,556503680,4,4,3,0,43878997
+baseline,RULE,10000,4096,true,0,5,366928440,353125000,695998768,803499336,25,101,3,1,43878994
+canonical,RULE,10000,4096,true,0,5,136851320,128125000,410457832,776276728,14,20,3,1,43878994
+canonical,RULE,10000,4096,true,1,5,135721680,128125000,410457832,790299296,17,25,3,1,43878994
+baseline,RULE,10000,4096,true,1,5,344581020,321875000,695998768,778090496,24,34,3,1,43878994
+baseline,RULE,10000,4096,true,2,5,345964000,325000000,695998768,793303176,23,57,3,1,43878994
+canonical,RULE,10000,4096,true,2,5,136770680,131250000,410457832,789566944,16,24,3,1,43878994
diff --git a/docs/benchmarks/websocket-reconciliation/run-2.csv 
b/docs/benchmarks/websocket-reconciliation/run-2.csv
new file mode 100644
index 0000000000..a5fd7389a6
--- /dev/null
+++ b/docs/benchmarks/websocket-reconciliation/run-2.csv
@@ -0,0 +1,145 @@
+variant,group,rows,handle_chars,changed,sample,iterations,wall_ns_per_tick,cpu_ns_per_tick,allocated_bytes_per_tick,peak_observed_heap_bytes,gc_count,gc_ms,load_calls_per_tick,publishes_per_tick,payload_chars
+baseline,PLUGIN,1000,256,false,0,5,1864500,0,2493792,32834936,0,0,3,0,376999
+canonical,PLUGIN,1000,256,false,0,5,2267220,3125000,2121040,31796896,0,0,3,0,376999
+canonical,PLUGIN,1000,256,false,1,5,1918120,0,2121040,31797968,0,0,3,0,376999
+baseline,PLUGIN,1000,256,false,1,5,1793760,3125000,2491280,32846664,0,0,3,0,376999
+baseline,PLUGIN,1000,256,false,2,5,1823500,3125000,2491280,32846792,0,0,3,0,376999
+canonical,PLUGIN,1000,256,false,2,5,1834900,3125000,2121040,31798344,0,0,3,0,376999
+baseline,PLUGIN,1000,256,true,0,5,3975340,3125000,9267152,68528672,0,0,3,1,376994
+canonical,PLUGIN,1000,256,true,0,5,1477540,0,3342936,37071520,0,0,3,1,376994
+canonical,PLUGIN,1000,256,true,1,5,1470340,0,3342936,37071656,0,0,3,1,376994
+baseline,PLUGIN,1000,256,true,1,5,3605040,6250000,9267152,68529168,0,0,3,1,376994
+baseline,PLUGIN,1000,256,true,2,5,3683560,3125000,9267152,68529320,0,0,3,1,376994
+canonical,PLUGIN,1000,256,true,2,5,1427260,0,3342936,37073048,0,0,3,1,376994
+baseline,SELECTOR,1000,256,false,0,5,1933060,0,3505184,45033256,0,0,3,0,552001
+canonical,SELECTOR,1000,256,false,0,5,2059260,0,2961040,37693344,0,0,3,0,552001
+canonical,SELECTOR,1000,256,false,1,5,2066680,3125000,2961040,37693480,0,0,3,0,552001
+baseline,SELECTOR,1000,256,false,1,5,1928020,3125000,3505184,45033848,0,0,3,0,552001
+baseline,SELECTOR,1000,256,false,2,5,2026440,3125000,3505184,45033976,0,0,3,0,552001
+canonical,SELECTOR,1000,256,false,2,5,1922880,0,2961040,37694072,0,0,3,0,552001
+baseline,SELECTOR,1000,256,true,0,5,5827100,6250000,12620808,95366024,0,0,3,1,551994
+canonical,SELECTOR,1000,256,true,0,5,2425040,3125000,4752240,50277384,0,0,3,1,551994
+canonical,SELECTOR,1000,256,true,1,5,2124220,3125000,4752240,51326104,0,0,3,1,551994
+baseline,SELECTOR,1000,256,true,1,5,6038260,6250000,12620976,95366424,0,0,3,1,551994
+baseline,SELECTOR,1000,256,true,2,5,5833200,3125000,12620808,96416016,0,0,3,1,551994
+canonical,SELECTOR,1000,256,true,2,5,2114240,3125000,4752240,51327432,0,0,3,1,551994
+baseline,RULE,1000,256,false,0,5,1763960,0,3455104,45017216,0,0,3,0,546997
+canonical,RULE,1000,256,false,0,5,2077720,3125000,2920880,37677304,0,0,3,0,546997
+canonical,RULE,1000,256,false,1,5,2021040,3125000,2920880,37677512,0,0,3,0,546997
+baseline,RULE,1000,256,false,1,5,1825620,3125000,3455104,45017672,0,0,3,0,546997
+baseline,RULE,1000,256,false,2,5,1777760,3125000,3455104,45017792,0,0,3,0,546997
+canonical,RULE,1000,256,false,2,5,1900820,3125000,2920880,37677816,0,0,3,0,546997
+baseline,RULE,1000,256,true,0,5,5487460,3125000,12550480,94301464,0,0,3,1,546994
+canonical,RULE,1000,256,true,0,5,2050460,3125000,4688624,50261392,0,0,3,1,546994
+canonical,RULE,1000,256,true,1,5,1994200,3125000,4688624,50261528,0,0,3,1,546994
+baseline,RULE,1000,256,true,1,5,5295300,6250000,12550480,94301904,0,0,3,1,546994
+baseline,RULE,1000,256,true,2,5,5358980,6250000,12550480,94302024,0,0,3,1,546994
+canonical,RULE,1000,256,true,2,5,1910820,3125000,4688624,50261952,0,0,3,1,546994
+baseline,PLUGIN,1000,4096,false,0,5,10156680,9375000,25499184,168486856,0,0,3,0,4216999
+canonical,PLUGIN,1000,4096,false,0,5,9401680,9375000,21289040,138078280,0,0,3,0,4216999
+canonical,PLUGIN,1000,4096,false,1,5,9272880,9375000,21289040,138078424,0,0,3,0,4216999
+baseline,PLUGIN,1000,4096,false,1,5,9857720,9375000,25499184,169535832,0,0,3,0,4216999
+baseline,PLUGIN,1000,4096,false,2,5,10265360,9375000,25499184,169536024,0,0,3,0,4216999
+canonical,PLUGIN,1000,4096,false,2,5,9469620,9375000,21289040,138078808,0,0,3,0,4216999
+baseline,PLUGIN,1000,4096,true,0,5,29745600,31250000,61804064,374009968,0,0,3,1,4216994
+canonical,PLUGIN,1000,4096,true,0,5,10170620,9375000,34243384,224063728,0,0,3,1,4216994
+canonical,PLUGIN,1000,4096,true,1,5,10188240,9375000,34243384,224063872,0,0,3,1,4216994
+baseline,PLUGIN,1000,4096,true,1,5,29940620,31250000,61804064,374010368,0,0,3,1,4216994
+baseline,PLUGIN,1000,4096,true,2,5,29095980,31250000,61804064,374010496,0,0,3,1,4216994
+canonical,PLUGIN,1000,4096,true,2,5,10058220,9375000,34243384,224064256,0,0,3,1,4216994
+baseline,SELECTOR,1000,4096,false,0,5,10355040,9375000,26545184,172988424,0,0,3,0,4392001
+canonical,SELECTOR,1000,4096,false,0,5,9837640,9375000,22161040,142579888,0,0,3,0,4392001
+canonical,SELECTOR,1000,4096,false,1,5,10027760,9375000,22161040,142580040,0,0,3,0,4392001
+baseline,SELECTOR,1000,4096,false,1,5,10499060,9375000,26545184,171940304,0,0,3,0,4392001
+baseline,SELECTOR,1000,4096,false,2,5,10420820,9375000,26545184,171940440,0,0,3,0,4392001
+canonical,SELECTOR,1000,4096,false,2,5,9863720,12500000,22161040,142580448,0,0,3,0,4392001
+baseline,SELECTOR,1000,4096,true,0,5,31730340,31250000,73503512,433036104,0,0,3,1,4391994
+canonical,SELECTOR,1000,4096,true,0,5,10515880,9375000,35652688,228563920,0,0,3,1,4391994
+canonical,SELECTOR,1000,4096,true,1,5,10457940,9375000,35652688,228564072,0,0,3,1,4391994
+baseline,SELECTOR,1000,4096,true,1,5,31023280,31250000,73503512,433036656,0,0,3,1,4391994
+baseline,SELECTOR,1000,4096,true,2,5,31234300,31250000,73503512,433036792,0,0,3,1,4391994
+canonical,SELECTOR,1000,4096,true,2,5,10879740,12500000,35652688,228564608,0,0,3,1,4391994
+baseline,RULE,1000,4096,false,0,5,10211720,9375000,26495104,171909840,0,0,3,0,4386997
+canonical,RULE,1000,4096,false,0,5,9591600,9375000,22120880,142549840,0,0,3,0,4386997
+canonical,RULE,1000,4096,false,1,5,10022860,12500000,22120880,142549984,0,0,3,0,4386997
+baseline,RULE,1000,4096,false,1,5,10286520,9375000,26495104,171912048,0,0,3,0,4386997
+baseline,RULE,1000,4096,false,2,5,10318340,9375000,26495104,171912176,0,0,3,0,4386997
+canonical,RULE,1000,4096,false,2,5,9637740,9375000,22120880,142552176,0,0,3,0,4386997
+baseline,RULE,1000,4096,true,0,5,33338040,31250000,73043280,417555224,3,3,3,1,4386994
+canonical,RULE,1000,4096,true,0,5,11007760,9375000,35589104,211265872,3,1,3,1,4386994
+canonical,RULE,1000,4096,true,1,5,11479920,12500000,35589104,211547840,3,2,3,1,4386994
+baseline,RULE,1000,4096,true,1,5,32383500,31250000,73043280,417560032,3,2,3,1,4386994
+baseline,RULE,1000,4096,true,2,5,32778180,34375000,73043280,417557816,3,2,3,1,4386994
+canonical,RULE,1000,4096,true,2,5,11042920,9375000,35589104,211548232,3,2,3,1,4386994
+baseline,PLUGIN,10000,256,false,0,5,16985080,12500000,24744968,147147256,3,2,3,0,3778999
+canonical,PLUGIN,10000,256,false,0,5,13427460,12500000,21047880,137015472,0,0,3,0,3778999
+canonical,PLUGIN,10000,256,false,1,5,13322100,12500000,21047880,137015624,0,0,3,0,3778999
+baseline,PLUGIN,10000,256,false,1,5,15853580,15625000,24744968,147168464,3,2,3,0,3778999
+baseline,PLUGIN,10000,256,false,2,5,16412440,12500000,24744968,147237456,3,2,3,0,3778999
+canonical,PLUGIN,10000,256,false,2,5,13137080,12500000,21047880,137016016,0,0,3,0,3778999
+baseline,PLUGIN,10000,256,true,0,5,42077140,40625000,91005208,494273912,3,2,3,1,3778994
+canonical,PLUGIN,10000,256,true,0,5,14946920,15625000,38851952,218637616,3,2,3,1,3778994
+canonical,PLUGIN,10000,256,true,1,5,15186160,15625000,38851952,218549200,3,2,3,1,3778994
+baseline,PLUGIN,10000,256,true,1,5,43134220,40625000,91005208,494222824,3,2,3,1,3778994
+baseline,PLUGIN,10000,256,true,2,5,41077940,40625000,91005208,495314056,3,3,3,1,3778994
+canonical,PLUGIN,10000,256,true,2,5,15020720,15625000,38851952,218641144,3,2,3,1,3778994
+baseline,SELECTOR,10000,256,false,0,5,22541920,21875000,35132968,205633848,3,2,3,0,5529001
+canonical,SELECTOR,10000,256,false,0,5,20727140,21875000,29623880,187249840,0,0,3,0,5529001
+canonical,SELECTOR,10000,256,false,1,5,20458860,21875000,29623880,188298568,0,0,3,0,5529001
+baseline,SELECTOR,10000,256,false,1,5,22908840,21875000,35132904,205514368,3,2,3,0,5529001
+baseline,SELECTOR,10000,256,false,2,5,22811620,18750000,35132872,205620728,3,2,3,0,5529001
+canonical,SELECTOR,10000,256,false,2,5,20683020,18750000,29623987,187250560,0,0,3,0,5529001
+baseline,SELECTOR,10000,256,true,0,5,64581460,65625000,133386200,614309056,4,6,3,1,5528994
+canonical,SELECTOR,10000,256,true,0,5,23799280,25000000,54977632,308520536,3,2,3,1,5528994
+canonical,SELECTOR,10000,256,true,1,5,24598360,25000000,54977632,308892880,3,2,3,1,5528994
+baseline,SELECTOR,10000,256,true,1,5,64294680,59375000,133386200,599813832,4,3,3,1,5528994
+baseline,SELECTOR,10000,256,true,2,5,63990040,59375000,133386200,720264832,3,2,3,1,5528994
+canonical,SELECTOR,10000,256,true,2,5,23584340,21875000,54977632,308619312,3,2,3,1,5528994
+baseline,RULE,10000,256,false,0,5,23840640,21875000,34632792,203380456,3,3,3,0,5478997
+canonical,RULE,10000,256,false,0,5,19929180,21875000,29223624,184796264,0,0,3,0,5478997
+canonical,RULE,10000,256,false,1,5,20672780,21875000,29223624,184796408,0,0,3,0,5478997
+baseline,RULE,10000,256,false,1,5,23137800,21875000,34632792,202842840,3,2,3,0,5478997
+baseline,RULE,10000,256,false,2,5,23776140,21875000,34632792,203372392,3,2,3,0,5478997
+canonical,RULE,10000,256,false,2,5,22240800,25000000,29223624,184796792,0,0,3,0,5478997
+baseline,RULE,10000,256,true,0,5,63958780,65625000,122493488,651967192,3,2,3,1,5478994
+canonical,RULE,10000,256,true,0,5,23102180,18750000,54232552,306527896,3,2,3,1,5478994
+canonical,RULE,10000,256,true,1,5,23340820,21875000,54232552,306269016,3,3,3,1,5478994
+baseline,RULE,10000,256,true,1,5,65041660,62500000,122493488,651110768,3,3,3,1,5478994
+baseline,RULE,10000,256,true,2,5,63764360,65625000,122493488,651232912,3,2,3,1,5478994
+canonical,RULE,10000,256,true,2,5,22495760,21875000,54232552,305915800,3,2,3,1,5478994
+baseline,PLUGIN,10000,4096,false,0,5,109510800,106250000,255144872,764953032,5,8,3,0,42178999
+canonical,PLUGIN,10000,4096,false,0,5,104497340,106250000,213047784,576391400,4,5,3,0,42178999
+canonical,PLUGIN,10000,4096,false,1,5,107202040,103125000,213047784,584312384,4,5,3,0,42178999
+baseline,PLUGIN,10000,4096,false,1,5,113774960,112500000,255144872,779625176,8,13,3,0,42178999
+baseline,PLUGIN,10000,4096,false,2,5,118799420,109375000,255144872,785925360,8,12,3,0,42178999
+canonical,PLUGIN,10000,4096,false,2,5,115225200,106250000,213047784,580925416,4,4,3,0,42178999
+baseline,PLUGIN,10000,4096,true,0,5,344984860,325000000,669390360,788679296,24,62,3,1,42178994
+canonical,PLUGIN,10000,4096,true,0,5,138727560,131250000,395077136,792810608,14,25,3,1,42178994
+canonical,PLUGIN,10000,4096,true,1,5,133619280,125000000,395077136,772867080,13,19,3,1,42178994
+baseline,PLUGIN,10000,4096,true,1,5,368195160,328125000,669390360,785397688,25,104,3,1,42178994
+baseline,PLUGIN,10000,4096,true,2,5,364216480,318750000,669390360,789845232,24,108,3,1,42178994
+canonical,PLUGIN,10000,4096,true,2,5,141413120,128125000,395077136,784193992,14,25,3,1,42178994
+baseline,SELECTOR,10000,4096,false,0,5,130290940,131250000,265532872,798468168,8,15,3,0,43929001
+canonical,SELECTOR,10000,4096,false,0,5,122482400,125000000,221623784,567823072,2,3,3,0,43929001
+canonical,SELECTOR,10000,4096,false,1,5,123664440,125000000,221623784,592457992,4,4,3,0,43929001
+baseline,SELECTOR,10000,4096,false,1,5,126436120,115625000,265532872,761746144,8,14,3,0,43929001
+baseline,SELECTOR,10000,4096,false,2,5,121089940,112500000,265532872,713071264,5,9,3,0,43929001
+canonical,SELECTOR,10000,4096,false,2,5,113049740,112500000,221623784,586193704,2,3,3,0,43929001
+baseline,SELECTOR,10000,4096,true,0,5,342809260,337500000,700524056,797539480,24,36,3,1,43928994
+canonical,SELECTOR,10000,4096,true,0,5,133265700,128125000,411202912,796416632,14,22,3,1,43928994
+canonical,SELECTOR,10000,4096,true,1,5,133051100,121875000,411202912,790075376,14,20,3,1,43928994
+baseline,SELECTOR,10000,4096,true,1,5,349317320,328125000,700524056,787537968,24,70,3,1,43928994
+baseline,SELECTOR,10000,4096,true,2,5,363940920,343750000,700524056,798086792,25,101,3,1,43928994
+canonical,SELECTOR,10000,4096,true,2,5,135609940,121875000,411202912,788742296,16,26,3,1,43928994
+baseline,RULE,10000,4096,false,0,5,117716580,115625000,265032792,784566848,8,13,3,0,43878997
+canonical,RULE,10000,4096,false,0,5,109273080,109375000,221223624,556574624,4,2,3,0,43878997
+canonical,RULE,10000,4096,false,1,5,109180900,106250000,221223624,591177784,4,5,3,0,43878997
+baseline,RULE,10000,4096,false,1,5,115030540,115625000,265032792,795046592,8,12,3,0,43878997
+baseline,RULE,10000,4096,false,2,5,115867380,109375000,265032792,756245288,8,10,3,0,43878997
+canonical,RULE,10000,4096,false,2,5,112065980,109375000,221223624,588796824,4,4,3,0,43878997
+baseline,RULE,10000,4096,true,0,5,342323480,321875000,695998768,789657680,22,63,3,1,43878994
+canonical,RULE,10000,4096,true,0,5,129975500,131250000,410457832,780722544,13,20,3,1,43878994
+canonical,RULE,10000,4096,true,1,5,132262200,128125000,410457832,762575328,14,29,3,1,43878994
+baseline,RULE,10000,4096,true,1,5,342821680,337500000,695998768,786938568,29,54,3,1,43878994
+baseline,RULE,10000,4096,true,2,5,355270200,328125000,695998768,796650448,25,66,3,1,43878994
+canonical,RULE,10000,4096,true,2,5,130826340,121875000,410457832,778680088,13,23,3,1,43878994
diff --git a/docs/websocket-reconciliation-benchmark.md 
b/docs/websocket-reconciliation-benchmark.md
new file mode 100644
index 0000000000..e97101958e
--- /dev/null
+++ b/docs/websocket-reconciliation-benchmark.md
@@ -0,0 +1,226 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# WebSocket reconciliation: canonical snapshot reuse
+
+This addresses [#7840](https://github.com/apache/shenyu/issues/7840). Only the
+admin-side reconciliation processing changes; field projection, gateway-side
+snapshot application and receive-side JSON conversion remain independent.
+
+## Representation and compatibility
+
+The canonical representation is the existing lexicographically sorted list of
+row JSON strings from `GsonUtils`. Both the digest and outgoing message reuse
+these strings. The alternative of retaining a JSON tree alongside each sorting
+string increases retained representations even for unchanged cycles; retaining
+strings alone also lets unchanged cycles avoid creating trees entirely.
+
+The digest is still MD5 of the UTF-8 encoding of `String.join("\n", rows)`.
+Updates feed each row and a single LF byte **between** rows to `MessageDigest`.
+There is no trailing delimiter, and the empty snapshot retains MD5 of the empty
+string. Ordering, duplicate rows, field changes, additions and deletions remain
+significant exactly as before. No digest migration is needed.
+
+For changed groups a Gson `JsonWriter` writes the `WebsocketData` envelope and
+inserts each canonical string with `jsonValue`. Values are generated by Gson,
+never accepted as unvalidated input. There is no intermediate whole-snapshot
+digest string/byte array and no per-row parse/tree/second serialization. The
+writer still buffers the final message, as required by the existing collector.
+Envelope fields, escaping and row order are checked against the old encoding,
+including Unicode, embedded JSON, quotes, newlines, nulls and duplicate rows.
+
+Every cycle still loads authoritative service state. Active namespace 
selection,
+master gating, opt-in scheduling and `REFRESH`/`fullSnapshot` semantics remain
+unchanged. Empty snapshots are sent to clear stale gateway state. A cursor only
+advances after the existing synchronous `push` call returns; this preserves the
+current publication contract and does not introduce a gateway acknowledgement.
+Load, row serialization, message writing and publication exceptions leave the
+previous cursor intact so the next cycle retries.
+
+## Reproduce the processing benchmark
+
+`WebsocketDataReconcilerBenchmark` is an opt-in test fixture with no new
+dependencies. It is outside normal Surefire test naming and additionally 
requires
+`shenyu.reconciliation.benchmark=true`. It compares a frozen copy of the old
+algorithm with the actual modified reconciler, using identical DTO shapes,
+deterministically shuffled rows and an in-memory replacement for service loads
+and publication. Scheduling is bypassed so a synchronous tick can be measured.
+
+Run from the repository root (use `./mvnw.cmd` in PowerShell, quoting each `-D`
+argument as shown):
+
+```sh
+./mvnw -pl shenyu-admin -am test \
+  '-Dtest=WebsocketDataReconcilerBenchmark' \
+  '-Dsurefire.failIfNoSpecifiedTests=false' \
+  '-Dshenyu.reconciliation.benchmark=true' \
+  '-Djacoco.skip=true' \
+  '-DargLine=-Xms768m -Xmx768m 
-XX:StartFlightRecording=filename=target/reconciliation.jfr,settings=profile,dumponexit=true'
+```
+
+Defaults are 20 warmup ticks per implementation/workload, three samples of five
+ticks, 1k/10k rows per PLUGIN/SELECTOR/RULE group and 256/4096-character
+config/handle strings. Selectors and rules also have a URI condition. Other
+groups are empty, so every tick still performs three service loads. Unchanged
+ticks emit nothing; changed ticks mutate one row's name without publishing an
+event, and must emit exactly one snapshot. The two implementations must produce
+identical final messages and digest cursors after every sample.
+
+Override positive `shenyu.benchmark.warmup`, `shenyu.benchmark.samples` and
+`shenyu.benchmark.iterations` for longer runs. Sample order alternates baseline
+and canonical implementations. Explicit GC runs **outside** each timed sample;
+the DTO fixtures remain live for both implementations. CPU time and allocated
+bytes come from the current reconciliation thread's JDK management counters.
+Wall time covers the complete synchronous tick including the three service
+loads. `peak_observed_heap_bytes` samples process heap use on a separate daemon
+thread with a requested 1 ms delay, including observations at the sample
+boundaries. It is an observed peak, not an exact maximum or live-set size; 
short
+peaks between observations can be missed. Sampling allocations are excluded 
from
+the reconciliation thread's allocation counter, but included in process 
heap/GC.
+GC counts/times cover only the measured batch. Setup, assertions and CSV 
writing
+are excluded from timing. On Windows, thread CPU accounting is quantized, so
+short batches can report zero CPU time; use longer batches for precise CPU
+comparisons of small snapshots.
+
+Raw samples are written to
+`shenyu-admin/target/websocket-reconciliation-benchmark.csv`; the recording is
+`shenyu-admin/target/reconciliation.jfr`. Retain the CSV, JFR, JVM arguments 
and
+host details for repeated runs. JFR spans both implementations, so use the
+`LegacyReconciler.reconcileSafely` and `WebsocketDataReconciler.reconcileGroup`
+stacks to distinguish them; thread counters supply exact per-sample allocation
+totals. This fixture measures serialization/digest/payload processing. Its 
three
+service calls per tick are **not measured database reads**; it does not measure
+WebSocket transport, gateway convergence latency or business-request p99.
+
+## Correctness checks
+
+```sh
+./mvnw -pl shenyu-admin,shenyu-sync-data-center/shenyu-sync-data-websocket -am 
test \
+  
'-Dtest=WebsocketDataReconcilerTest,WebsocketCollectorTest,ShenyuWebsocketClientTest,WebsocketDataHandlerTest'
 \
+  '-Dsurefire.failIfNoSpecifiedTests=false' '-Djacoco.skip=true'
+```
+
+The admin regressions compare both digest and complete wire messages with the 
old
+implementation and exercise changed order, unchanged polling, field changes,
+insertions, deletions, empty snapshots, load/serialization/publication 
failures,
+namespace isolation and master gating. Existing gateway regressions check
+authoritative cache replacement, removal of missing rules, empty snapshot
+clearing and preservation of other namespaces.
+
+## Deployment measurements required for end-to-end claims
+
+Follow [#7650](https://github.com/apache/shenyu/issues/7650) on the target
+deployment, with identical database contents, JVM resources, gateway/upstream
+versions, plugin configuration and load generator settings:
+
+1. Run baseline and canonical builds with reconciliation enabled and the same
+   interval, connected namespaces and master role. Also record a disabled 
control.
+2. Seed 1k/10k+ rows for each group, with both handle sizes. Warm the JVM and 
run
+   repeated unchanged cycles. Change rows directly in the shared database to
+   exercise missed-event recovery; include insertions, field edits, partial
+   deletion and deletion of the last row.
+3. Collect admin CPU, allocation/JFR, GC, peak heap, actual SQL counts and 
complete
+   tick duration. This optimization intentionally does not remove authoritative
+   database reads. Measure each deployment's queries rather than equating 
service
+   invocations to SQL statements.
+4. At a fixed business arrival rate collect successful throughput, errors,
+   p50/p95/p99 and synchronization convergence time. Include a direct-upstream
+   capacity control and verify final gateway state after each change. Exercise
+   publication failure/recovery and non-master/no-session controls.
+5. Retain raw results for multiple fresh JVM runs, report ranges, and compare
+   resource cost at equal successful throughput. Do not infer business latency
+   or end-to-end improvements from the isolated processing results below.
+
+## Local processing results
+
+Environment: 2026-10-19, Windows 11 amd64, AMD Ryzen 9 8945HX (16 cores/32
+logical processors), approximately 31.8 GiB RAM, Eclipse Temurin
+21.0.10+7-LTS, G1, `-Xms768m -Xmx768m`, JFR `profile`, no CPU affinity or 
quota.
+The checkout is based on `297ac6f84`; the baseline fixture freezes its original
+reconciliation algorithm. Both variants run in each fresh JVM against identical
+fixtures. This is a shared developer workstation, not an isolated throughput
+test environment.
+
+Two fresh JVM runs use the defaults above. Each workload/variant has six 
measured
+batches in total (three per JVM), with five ticks per batch. Raw samples:
+[run 1](benchmarks/websocket-reconciliation/run-1.csv) and
+[run 2](benchmarks/websocket-reconciliation/run-2.csv). Values below are 
medians
+of those six batches. CPU counters have the Windows granularity described 
above;
+allocation totals measure the reconciliation thread, not retained heap.
+
+| Group | Rows | Handle chars | State | Wall ms/tick, baseline → canonical | 
Allocated MiB/tick, baseline → canonical |
+| --- | ---: | ---: | --- | ---: | ---: |
+| PLUGIN | 1000 | 256 | unchanged | 1.84 → 1.88 | 2.38 → 2.02 |
+| PLUGIN | 1000 | 256 | changed | 3.86 → 1.47 | 8.82 → 3.19 |
+| PLUGIN | 1000 | 4096 | unchanged | 10.94 → 10.96 | 24.32 → 20.30 |
+| PLUGIN | 1000 | 4096 | changed | 34.21 → 11.50 | 58.94 → 32.66 |
+| PLUGIN | 10000 | 256 | unchanged | 18.32 → 13.38 | 23.60 → 20.07 |
+| PLUGIN | 10000 | 256 | changed | 51.56 → 17.70 | 86.79 → 37.05 |
+| PLUGIN | 10000 | 4096 | unchanged | 117.42 → 115.09 | 243.33 → 203.18 |
+| PLUGIN | 10000 | 4096 | changed | 362.54 → 136.17 | 638.38 → 376.77 |
+| RULE | 1000 | 256 | unchanged | 2.07 → 2.35 | 3.30 → 2.79 |
+| RULE | 1000 | 256 | changed | 5.58 → 2.40 | 11.97 → 4.47 |
+| RULE | 1000 | 4096 | unchanged | 12.16 → 11.64 | 25.27 → 21.10 |
+| RULE | 1000 | 4096 | changed | 39.57 → 12.83 | 69.66 → 33.94 |
+| RULE | 10000 | 256 | unchanged | 27.45 → 24.75 | 33.03 → 27.87 |
+| RULE | 10000 | 256 | changed | 69.12 → 25.20 | 116.82 → 51.72 |
+| RULE | 10000 | 4096 | unchanged | 118.33 → 112.75 | 252.75 → 210.98 |
+| RULE | 10000 | 4096 | changed | 345.27 → 133.99 | 663.76 → 391.44 |
+| SELECTOR | 1000 | 256 | unchanged | 1.95 → 2.06 | 3.34 → 2.82 |
+| SELECTOR | 1000 | 256 | changed | 5.85 → 2.11 | 12.04 → 4.53 |
+| SELECTOR | 1000 | 4096 | unchanged | 11.85 → 10.41 | 25.32 → 21.13 |
+| SELECTOR | 1000 | 4096 | changed | 36.92 → 11.73 | 70.10 → 34.00 |
+| SELECTOR | 10000 | 256 | unchanged | 26.95 → 25.72 | 33.51 → 28.25 |
+| SELECTOR | 10000 | 256 | changed | 77.53 → 29.32 | 127.21 → 52.43 |
+| SELECTOR | 10000 | 4096 | unchanged | 125.61 → 119.10 | 253.23 → 211.36 |
+| SELECTOR | 10000 | 4096 | changed | 371.51 → 142.68 | 668.07 → 392.15 |
+
+Across these shapes, changed-cycle median wall time decreases by 57.0–68.2%
+and allocation decreases by 41.0–63.9%. Unchanged-cycle allocation decreases by
+14.9–16.5%, but median wall time ranges from 13.4% slower to 27.0% faster;
+small-case sample ranges overlap. There is no claim of a universal unchanged
+latency improvement. For 10k/4096 changed groups, median thread CPU time is
+326.56 → 128.12 ms (PLUGIN/RULE) and 350.00 → 131.25 ms (SELECTOR).
+
+Across the 72 measured batches per variant/state, observed process heap peaks
+are at most 761.5 → 565.0 MiB for unchanged workloads and 766.3 → 763.6 MiB for
+changed workloads. Changed workloads can still approach the configured heap
+limit; reduced allocation does not guarantee a proportionately lower peak.
+Measured-batch GC counts total 183 → 58 (unchanged) and 519 → 328 (changed),
+with collection times 256 → 71 ms and 1459 → 485 ms respectively. These sums
+exclude explicit pre-sample GC and warmup, and are specific to this workload
+mix and G1 configuration.
+
+All 288 measured batches pass identical message/digest assertions and the
+expected three service loads per tick, zero unchanged publications and one
+changed publication. The separate regression run passes 108 admin/gateway 
tests,
+including actual gateway cache replacement and namespace-preserving deletion.
+Checkstyle and Apache RAT checks also pass for the admin and WebSocket sync
+modules.
+
+Before PR submission, the change was also applied to upstream `de93952d9` and
+129 admin/gateway regression tests passed, including the newer master-redirect
+tests. The reconciler and DTO source used by the processing benchmark are
+unchanged between the recorded benchmark base and this submission base.
+
+The retained JFR recordings are local build outputs
+`shenyu-admin/target/reconciliation-run-1.jfr` and
+`shenyu-admin/target/reconciliation-run-2.jfr`.
+
+Database reads, WebSocket transport, full gateway convergence latency and
+business p99 were not measured locally. The processing results establish a
+benefit for this stage; deployment acceptance still requires the procedure 
above.
diff --git 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/websocket/WebsocketDataReconciler.java
 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/websocket/WebsocketDataReconciler.java
index b4018bc3d1..9c49caf98a 100644
--- 
a/shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/websocket/WebsocketDataReconciler.java
+++ 
b/shenyu-admin/src/main/java/org/apache/shenyu/admin/listener/websocket/WebsocketDataReconciler.java
@@ -17,6 +17,8 @@
 
 package org.apache.shenyu.admin.listener.websocket;
 
+import com.google.gson.stream.JsonWriter;
+import org.apache.commons.codec.binary.Hex;
 import org.apache.commons.codec.digest.DigestUtils;
 import org.apache.shenyu.admin.config.properties.ClusterProperties;
 import org.apache.shenyu.admin.config.properties.WebsocketSyncProperties;
@@ -30,7 +32,6 @@ import org.apache.shenyu.admin.service.ProxySelectorService;
 import org.apache.shenyu.admin.service.RuleService;
 import org.apache.shenyu.admin.service.SelectorService;
 import org.apache.shenyu.common.concurrent.ShenyuThreadFactory;
-import org.apache.shenyu.common.dto.WebsocketData;
 import org.apache.shenyu.common.enums.ConfigGroupEnum;
 import org.apache.shenyu.common.enums.DataEventTypeEnum;
 import org.apache.shenyu.common.utils.GsonUtils;
@@ -40,10 +41,11 @@ import org.springframework.beans.factory.DisposableBean;
 import org.springframework.beans.factory.InitializingBean;
 import org.springframework.beans.factory.ObjectProvider;
 
+import java.io.IOException;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
 import java.util.Collections;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonParser;
-import java.util.stream.Collectors;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -53,6 +55,7 @@ import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.ScheduledThreadPoolExecutor;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 /**
  * Reconciles the configuration pushed through WebSocket data sync with the 
state
@@ -208,21 +211,16 @@ public class WebsocketDataReconciler implements 
InitializingBean, DisposableBean
                     .map(GsonUtils.getInstance()::toJson)
                     .sorted()
                     .collect(Collectors.toList());
-            String digest = DigestUtils.md5Hex(String.join("\n", rows));
+            String digest = digest(rows);
             if (digest.equals(digestCursor.get(cursorKey))) {
                 LOG.debug("websocket reconciliation group {} in namespace {} 
is unchanged, skip push",
                         group, namespaceId);
                 return;
             }
-            List<JsonElement> dataList = 
rows.stream().map(JsonParser::parseString).collect(Collectors.toList());
-            WebsocketData<?> websocketData =
-                    new WebsocketData<>(group.name(), 
DataEventTypeEnum.REFRESH.name(), dataList);
-            websocketData.setNamespaceId(namespaceId);
-            websocketData.setFullSnapshot(true);
-            push(namespaceId, GsonUtils.getInstance().toJson(websocketData));
+            push(namespaceId, snapshotMessage(namespaceId, group, rows));
             digestCursor.put(cursorKey, digest);
             LOG.info("websocket reconciliation pushed group {} for namespace 
{}, size: {}",
-                    group, namespaceId, dataList.size());
+                    group, namespaceId, rows.size());
         } catch (Exception e) {
             // the cursor is not advanced, so the next cycle retries this group
             LOG.error("websocket reconciliation failed for group {} in 
namespace {}, cursor not advanced",
@@ -230,6 +228,38 @@ public class WebsocketDataReconciler implements 
InitializingBean, DisposableBean
         }
     }
 
+    private String digest(final List<String> rows) {
+        MessageDigest digest = DigestUtils.getMd5Digest();
+        for (int index = 0; index < rows.size(); index++) {
+            // Match the previous MD5 of String.join("\n", rows), including no 
trailing delimiter.
+            if (index > 0) {
+                digest.update((byte) '\n');
+            }
+            digest.update(rows.get(index).getBytes(StandardCharsets.UTF_8));
+        }
+        return Hex.encodeHexString(digest.digest());
+    }
+
+    private String snapshotMessage(final String namespaceId, final 
ConfigGroupEnum group, final List<String> rows) throws IOException {
+        StringWriter message = new StringWriter();
+        try (JsonWriter writer = GsonUtils.getGson().newJsonWriter(message)) {
+            // Keep the WebsocketData envelope and reuse the canonical row 
JSON verbatim.
+            // Rows come only from Gson above, so they need neither reparsing 
nor escaping again.
+            writer.beginObject();
+            writer.name("groupType").value(group.name());
+            writer.name("eventType").value(DataEventTypeEnum.REFRESH.name());
+            writer.name("data").beginArray();
+            for (String row : rows) {
+                writer.jsonValue(row);
+            }
+            writer.endArray();
+            writer.name("namespaceId").value(namespaceId);
+            writer.name("fullSnapshot").value(true);
+            writer.endObject();
+        }
+        return message.toString();
+    }
+
     /**
      * Push one refresh message to the sessions of a namespace, visible for 
tests.
      *
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketDataReconcilerBenchmark.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketDataReconcilerBenchmark.java
new file mode 100644
index 0000000000..db71c80e74
--- /dev/null
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketDataReconcilerBenchmark.java
@@ -0,0 +1,323 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.admin.listener.websocket;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParser;
+import com.sun.management.ThreadMXBean;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.shenyu.admin.config.properties.ClusterProperties;
+import org.apache.shenyu.admin.config.properties.WebsocketSyncProperties;
+import org.apache.shenyu.admin.service.NamespacePluginService;
+import org.apache.shenyu.admin.service.RuleService;
+import org.apache.shenyu.admin.service.SelectorService;
+import org.apache.shenyu.common.concurrent.ShenyuThreadFactory;
+import org.apache.shenyu.common.dto.BaseData;
+import org.apache.shenyu.common.dto.ConditionData;
+import org.apache.shenyu.common.dto.PluginData;
+import org.apache.shenyu.common.dto.RuleData;
+import org.apache.shenyu.common.dto.SelectorData;
+import org.apache.shenyu.common.dto.WebsocketData;
+import org.apache.shenyu.common.enums.ConfigGroupEnum;
+import org.apache.shenyu.common.enums.DataEventTypeEnum;
+import org.apache.shenyu.common.utils.GsonUtils;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.lang.management.ManagementFactory;
+import java.lang.reflect.Proxy;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Opt-in, dependency-free comparison of reconciliation processing, with 
in-memory service loads.
+ * See docs/websocket-reconciliation-benchmark.md for commands and the 
integration measurement boundary.
+ */
+@EnabledIfSystemProperty(named = "shenyu.reconciliation.benchmark", matches = 
"true")
+public final class WebsocketDataReconcilerBenchmark {
+
+    private static final String NAMESPACE = "benchmark-namespace";
+
+    private static final List<ConfigGroupEnum> GROUPS = 
List.of(ConfigGroupEnum.PLUGIN, ConfigGroupEnum.SELECTOR, ConfigGroupEnum.RULE);
+
+    private final ThreadMXBean threads = (ThreadMXBean) 
ManagementFactory.getThreadMXBean();
+
+    private volatile long peakHeap;
+
+    private final int warmup = Integer.getInteger("shenyu.benchmark.warmup", 
20);
+
+    private final int iterations = 
Integer.getInteger("shenyu.benchmark.iterations", 5);
+
+    private final int samples = Integer.getInteger("shenyu.benchmark.samples", 
3);
+
+    @Test
+    public void benchmark() throws IOException {
+        assertTrue(threads.isThreadAllocatedMemorySupported());
+        assertTrue(threads.isCurrentThreadCpuTimeSupported());
+        assertTrue(warmup > 0 && iterations > 0 && samples > 0);
+        threads.setThreadAllocatedMemoryEnabled(true);
+        threads.setThreadCpuTimeEnabled(true);
+        Logger logger = (Logger) 
LoggerFactory.getLogger(WebsocketDataReconciler.class);
+        Level previous = logger.getLevel();
+        logger.setLevel(Level.OFF);
+        ScheduledThreadPoolExecutor sampler = new 
ScheduledThreadPoolExecutor(1, ShenyuThreadFactory.create("benchmark-heap", 
true));
+        sampler.scheduleWithFixedDelay(this::sampleHeap, 0, 1, 
TimeUnit.MILLISECONDS);
+        List<String> results = new ArrayList<>();
+        
results.add("variant,group,rows,handle_chars,changed,sample,iterations,wall_ns_per_tick,cpu_ns_per_tick,allocated_bytes_per_tick,"
+                + 
"peak_observed_heap_bytes,gc_count,gc_ms,load_calls_per_tick,publishes_per_tick,payload_chars");
+        try {
+            for (int size : List.of(1000, 10000)) {
+                for (int handleSize : List.of(256, 4096)) {
+                    for (ConfigGroupEnum group : GROUPS) {
+                        compare(group, size, handleSize, false, results);
+                        compare(group, size, handleSize, true, results);
+                    }
+                }
+            }
+            Path output = 
Path.of(System.getProperty("shenyu.benchmark.output", 
"target/websocket-reconciliation-benchmark.csv"));
+            Files.createDirectories(output.toAbsolutePath().getParent());
+            Files.write(output, results);
+            LoggerFactory.getLogger(getClass()).info("Reconciliation 
benchmark: JDK {}, processors {}, max heap {}, JVM args {}, CSV {}",
+                    System.getProperty("java.runtime.version"), 
Runtime.getRuntime().availableProcessors(), Runtime.getRuntime().maxMemory(),
+                    ManagementFactory.getRuntimeMXBean().getInputArguments(), 
output.toAbsolutePath());
+        } finally {
+            sampler.shutdownNow();
+            logger.setLevel(previous);
+        }
+    }
+
+    private void compare(final ConfigGroupEnum group, final int size, final 
int handleSize, final boolean changed, final List<String> results) {
+        Fixture baseline = new Fixture(group, size, handleSize);
+        Fixture canonical = new Fixture(group, size, handleSize);
+        try (BenchmarkReconciler oldReconciler = new 
LegacyReconciler(baseline);
+                BenchmarkReconciler newReconciler = new 
BenchmarkReconciler(canonical)) {
+            oldReconciler.reconcileSafely();
+            newReconciler.reconcileSafely();
+            for (int index = 0; index < warmup; index++) {
+                tick(oldReconciler, changed);
+                tick(newReconciler, changed);
+            }
+            for (int sample = 0; sample < samples; sample++) {
+                // Alternate order to expose, rather than consistently favor, 
JIT/thermal/order effects.
+                if (sample % 2 == 0) {
+                    results.add(measure("baseline", oldReconciler, size, 
handleSize, changed, sample));
+                    results.add(measure("canonical", newReconciler, size, 
handleSize, changed, sample));
+                } else {
+                    results.add(measure("canonical", newReconciler, size, 
handleSize, changed, sample));
+                    results.add(measure("baseline", oldReconciler, size, 
handleSize, changed, sample));
+                }
+                assertEquals(oldReconciler.getDigestCursor(), 
newReconciler.getDigestCursor());
+                assertEquals(baseline.message, canonical.message);
+            }
+        }
+    }
+
+    private String measure(final String variant, final BenchmarkReconciler 
reconciler, final int size,
+                           final int handleSize, final boolean changed, final 
int sample) {
+        Fixture fixture = reconciler.fixture;
+        fixture.loads = 0;
+        fixture.publications = 0;
+        System.gc();
+        resetPeakHeap();
+        final long gcCount = gcCount();
+        final long gcMillis = gcMillis();
+        long threadId = Thread.currentThread().getId();
+        long allocated = threads.getThreadAllocatedBytes(threadId);
+        long cpu = threads.getCurrentThreadCpuTime();
+        long start = System.nanoTime();
+        for (int index = 0; index < iterations; index++) {
+            tick(reconciler, changed);
+        }
+        final long elapsed = System.nanoTime() - start;
+        final long cpuElapsed = threads.getCurrentThreadCpuTime() - cpu;
+        final long allocatedBytes = threads.getThreadAllocatedBytes(threadId) 
- allocated;
+        sampleHeap();
+        long observedHeap = peakHeap;
+        assertEquals(3 * iterations, fixture.loads);
+        assertEquals(changed ? iterations : 0, fixture.publications);
+        return String.format(Locale.ROOT, 
"%s,%s,%d,%d,%s,%d,%d,%.0f,%.0f,%.0f,%d,%d,%d,%d,%d,%d",
+                variant, fixture.group, size, handleSize, changed, sample, 
iterations, elapsed / (double) iterations,
+                cpuElapsed / (double) iterations, allocatedBytes / (double) 
iterations, observedHeap,
+                gcCount() - gcCount, gcMillis() - gcMillis, fixture.loads / 
iterations, fixture.publications / iterations, fixture.message.length());
+    }
+
+    private synchronized void sampleHeap() {
+        peakHeap = Math.max(peakHeap, 
ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed());
+    }
+
+    private synchronized void resetPeakHeap() {
+        peakHeap = 
ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed();
+    }
+
+    private void tick(final BenchmarkReconciler reconciler, final boolean 
changed) {
+        if (changed) {
+            reconciler.fixture.change();
+        }
+        reconciler.reconcileSafely();
+    }
+
+    private long gcCount() {
+        return 
ManagementFactory.getGarbageCollectorMXBeans().stream().mapToLong(bean -> 
Math.max(0, bean.getCollectionCount())).sum();
+    }
+
+    private long gcMillis() {
+        return 
ManagementFactory.getGarbageCollectorMXBeans().stream().mapToLong(bean -> 
Math.max(0, bean.getCollectionTime())).sum();
+    }
+
+    private static List<BaseData> rows(final ConfigGroupEnum group, final int 
size, final int handleSize) {
+        String handle = "{\"value\":\"" + "x".repeat(handleSize - 12) + "\"}";
+        List<BaseData> result = new ArrayList<>(size);
+        for (int index = 0; index < size; index++) {
+            String id = String.format(Locale.ROOT, "%08d", index);
+            ConditionData condition = new ConditionData();
+            condition.setParamType("uri");
+            condition.setOperator("match");
+            condition.setParamName("/");
+            condition.setParamValue("/benchmark/" + id);
+            BaseData row;
+            switch (group) {
+                case PLUGIN:
+                    row = PluginData.builder().id(id).name("plugin-" + 
id).config(handle).enabled(true).sort(index).namespaceId(NAMESPACE).build();
+                    break;
+                case SELECTOR:
+                    row = SelectorData.builder().id(id).name("selector-" + 
id).pluginId("plugin").pluginName("divide")
+                            
.handle(handle).conditionList(List.of(condition)).matchMode(0).type(1).sort(index).enabled(true).namespaceId(NAMESPACE).build();
+                    break;
+                case RULE:
+                    row = RuleData.builder().id(id).name("rule-" + 
id).selectorId("selector").pluginName("divide")
+                            
.handle(handle).conditionDataList(List.of(condition)).matchMode(0).sort(index).enabled(true).namespaceId(NAMESPACE).build();
+                    break;
+                default:
+                    throw new IllegalArgumentException(group.name());
+            }
+            result.add(row);
+        }
+        Collections.shuffle(result, new java.util.Random(7840));
+        return result;
+    }
+
+    private static final class Fixture {
+
+        private final ConfigGroupEnum group;
+
+        private final List<BaseData> data;
+
+        private final String messagePrefix;
+
+        private String message;
+
+        private int revision;
+
+        private int loads;
+
+        private int publications;
+
+        private Fixture(final ConfigGroupEnum group, final int size, final int 
handleSize) {
+            this.group = group;
+            this.data = rows(group, size, handleSize);
+            this.messagePrefix = "{\"groupType\":\"" + group.name() + "\"";
+        }
+
+        private void change() {
+            data.get(0).setName("revision-" + (++revision % 2));
+        }
+
+        private List<?> load(final ConfigGroupEnum requested) {
+            loads++;
+            return requested == group ? data : Collections.emptyList();
+        }
+
+        private <T> T service(final Class<T> type, final ConfigGroupEnum 
requested) {
+            return type.cast(Proxy.newProxyInstance(type.getClassLoader(), new 
Class<?>[]{type}, (proxy, method, args) -> load(requested)));
+        }
+    }
+
+    private static class BenchmarkReconciler extends WebsocketDataReconciler 
implements AutoCloseable {
+
+        private final Fixture fixture;
+
+        protected BenchmarkReconciler(final Fixture fixture) {
+            super(new WebsocketSyncProperties(), new ClusterProperties(), 
null, null,
+                    fixture.service(NamespacePluginService.class, 
ConfigGroupEnum.PLUGIN),
+                    fixture.service(SelectorService.class, 
ConfigGroupEnum.SELECTOR),
+                    fixture.service(RuleService.class, ConfigGroupEnum.RULE), 
null, null, null, null);
+            this.fixture = fixture;
+        }
+
+        @Override
+        Set<String> activeNamespaceIds() {
+            return Set.of(NAMESPACE);
+        }
+
+        @Override
+        void push(final String namespaceId, final String message) {
+            fixture.publications++;
+            if (message.startsWith(fixture.messagePrefix)) {
+                fixture.message = message;
+            }
+        }
+
+        @Override
+        public void close() {
+            destroy();
+        }
+    }
+
+    /**
+     * Frozen pre-optimization algorithm; intentionally retains its 
join/parse/serialize steps for comparison.
+     */
+    private static final class LegacyReconciler extends BenchmarkReconciler {
+
+        private LegacyReconciler(final Fixture fixture) {
+            super(fixture);
+        }
+
+        @Override
+        void reconcileSafely() {
+            for (ConfigGroupEnum group : GROUPS) {
+                String key = NAMESPACE + ":" + group.name();
+                List<String> rows = 
super.fixture.load(group).stream().map(GsonUtils.getInstance()::toJson).sorted().collect(Collectors.toList());
+                String digest = DigestUtils.md5Hex(String.join("\n", rows));
+                if (digest.equals(getDigestCursor().get(key))) {
+                    continue;
+                }
+                List<JsonElement> trees = 
rows.stream().map(JsonParser::parseString).collect(Collectors.toList());
+                WebsocketData<?> message = new WebsocketData<>(group.name(), 
DataEventTypeEnum.REFRESH.name(), trees);
+                message.setNamespaceId(NAMESPACE);
+                message.setFullSnapshot(true);
+                push(NAMESPACE, GsonUtils.getInstance().toJson(message));
+                getDigestCursor().put(key, digest);
+            }
+        }
+    }
+}
diff --git 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketDataReconcilerTest.java
 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketDataReconcilerTest.java
index 37e9ece15f..fdc4c730be 100644
--- 
a/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketDataReconcilerTest.java
+++ 
b/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketDataReconcilerTest.java
@@ -17,6 +17,10 @@
 
 package org.apache.shenyu.admin.listener.websocket;
 
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParser;
+import org.apache.commons.codec.digest.DigestUtils;
 import org.apache.shenyu.admin.config.properties.ClusterProperties;
 import org.apache.shenyu.admin.config.properties.WebsocketSyncProperties;
 import org.apache.shenyu.admin.mode.cluster.service.ClusterSelectMasterService;
@@ -36,7 +40,10 @@ import org.apache.shenyu.common.dto.ProxyApiKeyData;
 import org.apache.shenyu.common.dto.ProxySelectorData;
 import org.apache.shenyu.common.dto.RuleData;
 import org.apache.shenyu.common.dto.SelectorData;
+import org.apache.shenyu.common.dto.WebsocketData;
+import org.apache.shenyu.common.enums.ConfigGroupEnum;
 import org.apache.shenyu.common.enums.DataEventTypeEnum;
+import org.apache.shenyu.common.utils.GsonUtils;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -46,21 +53,28 @@ import org.mockito.MockedStatic;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.springframework.beans.factory.ObjectProvider;
 
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.contains;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.mockStatic;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
@@ -166,6 +180,9 @@ public final class WebsocketDataReconcilerTest {
             // unchanged state: the second cycle pushes nothing
             reconciler.reconcileSafely();
             mocked.verifySends(NAMESPACE_1, 3);
+            verify(namespacePluginService, times(2)).listAll(NAMESPACE_1);
+            verify(selectorService, 
times(2)).listAllByNamespaceId(NAMESPACE_1);
+            verify(ruleService, times(2)).listAllByNamespaceId(NAMESPACE_1);
         }
     }
 
@@ -183,6 +200,11 @@ public final class WebsocketDataReconcilerTest {
                     .orElse("");
             assertTrue(ruleMessage.contains("\"eventType\":\"REFRESH\""), 
ruleMessage);
             assertTrue(ruleMessage.contains("\"data\":[]"), ruleMessage);
+            assertTrue(ruleMessage.contains("\"fullSnapshot\":true"), 
ruleMessage);
+            
assertTrue(ruleMessage.contains("\"namespaceId\":\"namespace-1\""), 
ruleMessage);
+            assertEquals(DigestUtils.md5Hex(""), 
reconciler.getDigestCursor().get(NAMESPACE_1 + ":RULE"));
+            reconciler.reconcileSafely();
+            mocked.verifySends(NAMESPACE_1, 3);
         }
     }
 
@@ -233,6 +255,8 @@ public final class WebsocketDataReconcilerTest {
         try (ReconciledCollector mocked = new 
ReconciledCollector(Set.of(NAMESPACE_1))) {
             reconciler.reconcileSafely();
             mocked.verifyNoSends();
+            verifyNoInteractions(appAuthService, namespacePluginService, 
selectorService, ruleService,
+                    metaDataService, proxySelectorService, 
discoveryUpstreamService, aiProxyApiKeyService);
         }
     }
 
@@ -265,6 +289,142 @@ public final class WebsocketDataReconcilerTest {
         }
     }
 
+    @Test
+    public void testCanonicalSnapshotsMatchPreviousDigestAndWireFormat() {
+        String namespaceId = "namespace-中文-<>&\"\\\n😀";
+        String handle = "{\"value\":\"中文😀 <>&=' \\\" \\\\ 
\\n\",\"empty\":null}";
+        PluginData plugin = 
PluginData.builder().id("plugin-1").name("divide").config(handle).namespaceId(namespaceId).build();
+        SelectorData selector = 
SelectorData.builder().id("selector-1").handle(handle).namespaceId(namespaceId).build();
+        RuleData first = new RuleData().setId("rule-1").setHandle(handle);
+        RuleData second = new RuleData().setId("rule-2").setHandle("");
+        List<PluginData> plugins = List.of(plugin);
+        List<SelectorData> selectors = List.of(selector);
+        List<RuleData> rules = Arrays.asList(second, first, null, first);
+        when(namespacePluginService.listAll(namespaceId)).thenReturn(plugins);
+        
when(selectorService.listAllByNamespaceId(namespaceId)).thenReturn(selectors);
+        when(ruleService.listAllByNamespaceId(namespaceId)).thenReturn(rules);
+        try (ReconciledCollector mocked = new 
ReconciledCollector(Set.of(namespaceId))) {
+            reconciler.reconcileSafely();
+            List<String> messages = mocked.capturedMessages(namespaceId);
+            assertEquals(3, messages.size());
+            assertPreviousEncoding(namespaceId, ConfigGroupEnum.PLUGIN, 
plugins, messages);
+            assertPreviousEncoding(namespaceId, ConfigGroupEnum.SELECTOR, 
selectors, messages);
+            assertPreviousEncoding(namespaceId, ConfigGroupEnum.RULE, rules, 
messages);
+        }
+    }
+
+    @Test
+    public void 
testFieldChangesAdditionsAndDeletionsAreDetectedWithoutEvents() {
+        stubAllGroups();
+        RuleData first = new RuleData().setId("rule-1").setHandle("old");
+        RuleData changed = new RuleData().setId("rule-1").setHandle("new");
+        RuleData added = new RuleData().setId("rule-2");
+        
when(ruleService.listAllByNamespaceId(NAMESPACE_1)).thenReturn(List.of(first), 
List.of(changed),
+                List.of(added, changed), List.of(added), 
Collections.emptyList(), Collections.emptyList());
+        try (ReconciledCollector mocked = new 
ReconciledCollector(Set.of(NAMESPACE_1))) {
+            for (int cycle = 0; cycle < 6; cycle++) {
+                reconciler.reconcileSafely();
+            }
+            List<String> rules = mocked.capturedMessages(NAMESPACE_1).stream()
+                    .filter(message -> 
message.contains("\"groupType\":\"RULE\""))
+                    .collect(Collectors.toList());
+            assertEquals(5, rules.size());
+            assertTrue(rules.get(1).contains("\"handle\":\"new\""));
+            assertEquals(2, 
JsonParser.parseString(rules.get(2)).getAsJsonObject().getAsJsonArray("data").size());
+            assertFalse(rules.get(3).contains("rule-1"));
+            assertTrue(rules.get(4).contains("\"data\":[]"));
+            mocked.verifySends(NAMESPACE_1, 7);
+        }
+    }
+
+    @Test
+    public void testFailedSerializationKeepsPreviousCursorAndRetries() {
+        stubAllGroups();
+        RuleData changed = new RuleData().setId("changed");
+        
when(ruleService.listAllByNamespaceId(NAMESPACE_1)).thenReturn(List.of(new 
RuleData()), List.of(changed));
+        Gson gson = GsonUtils.getGson();
+        GsonUtils serializer = spy(GsonUtils.getInstance());
+        try (ReconciledCollector mocked = new 
ReconciledCollector(Set.of(NAMESPACE_1))) {
+            reconciler.reconcileSafely();
+            String previousDigest = 
reconciler.getDigestCursor().get(NAMESPACE_1 + ":RULE");
+            doThrow(new IllegalArgumentException("serialization 
failed")).doCallRealMethod().when(serializer).toJson(changed);
+            try (MockedStatic<GsonUtils> mockedGson = 
mockStatic(GsonUtils.class)) {
+                mockedGson.when(GsonUtils::getInstance).thenReturn(serializer);
+                mockedGson.when(GsonUtils::getGson).thenReturn(gson);
+                reconciler.reconcileSafely();
+                assertEquals(previousDigest, 
reconciler.getDigestCursor().get(NAMESPACE_1 + ":RULE"));
+                mocked.verifySends(NAMESPACE_1, 3);
+                reconciler.reconcileSafely();
+                mocked.verifySends(NAMESPACE_1, 4);
+                assertEquals(DigestUtils.md5Hex(serializer.toJson(changed)), 
reconciler.getDigestCursor().get(NAMESPACE_1 + ":RULE"));
+            }
+        }
+    }
+
+    @Test
+    public void testFailedPublicationDoesNotCreateCursor() {
+        stubAllGroups();
+        try (ReconciledCollector mocked = new 
ReconciledCollector(Set.of(NAMESPACE_1))) {
+            mocked.failNextRuleSend(NAMESPACE_1);
+            reconciler.reconcileSafely();
+            assertFalse(reconciler.getDigestCursor().containsKey(NAMESPACE_1 + 
":RULE"));
+            assertEquals(2, reconciler.getDigestCursor().size());
+            reconciler.reconcileSafely();
+            assertTrue(reconciler.getDigestCursor().containsKey(NAMESPACE_1 + 
":RULE"));
+            mocked.verifySends(NAMESPACE_1, 4);
+        }
+    }
+
+    @Test
+    public void testFailedMessageWritingKeepsPreviousCursorAndRetries() {
+        stubAllGroups();
+        RuleData changed = new RuleData().setId("changed");
+        
when(ruleService.listAllByNamespaceId(NAMESPACE_1)).thenReturn(List.of(new 
RuleData()), List.of(changed));
+        Gson gson = GsonUtils.getGson();
+        GsonUtils serializer = GsonUtils.getInstance();
+        try (ReconciledCollector mocked = new 
ReconciledCollector(Set.of(NAMESPACE_1))) {
+            reconciler.reconcileSafely();
+            String previousDigest = 
reconciler.getDigestCursor().get(NAMESPACE_1 + ":RULE");
+            try (MockedStatic<GsonUtils> mockedGson = 
mockStatic(GsonUtils.class)) {
+                mockedGson.when(GsonUtils::getInstance).thenReturn(serializer);
+                mockedGson.when(GsonUtils::getGson).thenThrow(new 
IllegalStateException("writer failed")).thenReturn(gson);
+                reconciler.reconcileSafely();
+                assertEquals(previousDigest, 
reconciler.getDigestCursor().get(NAMESPACE_1 + ":RULE"));
+                mocked.verifySends(NAMESPACE_1, 3);
+                reconciler.reconcileSafely();
+                mocked.verifySends(NAMESPACE_1, 4);
+                assertEquals(DigestUtils.md5Hex(serializer.toJson(changed)), 
reconciler.getDigestCursor().get(NAMESPACE_1 + ":RULE"));
+            }
+        }
+    }
+
+    @Test
+    public void 
testFailedPublicationKeepsPreviousCursorAndRetriesEmptySnapshot() {
+        stubAllGroups();
+        
when(ruleService.listAllByNamespaceId(NAMESPACE_1)).thenReturn(List.of(new 
RuleData()), Collections.emptyList());
+        try (ReconciledCollector mocked = new 
ReconciledCollector(Set.of(NAMESPACE_1))) {
+            reconciler.reconcileSafely();
+            String previousDigest = 
reconciler.getDigestCursor().get(NAMESPACE_1 + ":RULE");
+            mocked.failNextRuleSend(NAMESPACE_1);
+            reconciler.reconcileSafely();
+            assertEquals(previousDigest, 
reconciler.getDigestCursor().get(NAMESPACE_1 + ":RULE"));
+            reconciler.reconcileSafely();
+            assertEquals(DigestUtils.md5Hex(""), 
reconciler.getDigestCursor().get(NAMESPACE_1 + ":RULE"));
+            reconciler.reconcileSafely();
+            mocked.verifySends(NAMESPACE_1, 5);
+        }
+    }
+
+    private void assertPreviousEncoding(final String namespaceId, final 
ConfigGroupEnum group, final List<?> data, final List<String> messages) {
+        List<String> rows = 
data.stream().map(GsonUtils.getInstance()::toJson).sorted().collect(Collectors.toList());
+        assertEquals(DigestUtils.md5Hex(String.join("\n", rows)), 
reconciler.getDigestCursor().get(namespaceId + ":" + group.name()));
+        List<JsonElement> trees = 
rows.stream().map(JsonParser::parseString).collect(Collectors.toList());
+        WebsocketData<?> expected = new WebsocketData<>(group.name(), 
DataEventTypeEnum.REFRESH.name(), trees);
+        expected.setNamespaceId(namespaceId);
+        expected.setFullSnapshot(true);
+        
assertTrue(messages.contains(GsonUtils.getInstance().toJson(expected)), 
group.name());
+    }
+
     private void stubAllGroups() {
         lenient().when(appAuthService.listAllByNamespaceId(anyString()))
                 .thenReturn(Collections.singletonList(new AppAuthData()));
@@ -309,6 +469,11 @@ public final class WebsocketDataReconcilerTest {
             mocked.verify(() -> WebsocketCollector.send(anyString(), 
anyString(), any()), never());
         }
 
+        private void failNextRuleSend(final String namespaceId) {
+            mocked.when(() -> WebsocketCollector.send(eq(namespaceId), 
contains("\"groupType\":\"RULE\""), eq(DataEventTypeEnum.REFRESH)))
+                    .thenThrow(new IllegalStateException("publication 
failed")).thenAnswer(invocation -> null);
+        }
+
         private List<String> capturedMessages(final String namespaceId) {
             mocked.verify(() -> WebsocketCollector.send(eq(namespaceId), 
messageCaptor.capture(),
                     eq(DataEventTypeEnum.REFRESH)), atLeast(0));

Reply via email to