github.com/chainreactors/fingers@v1.2.1/fingers/engine_perf_test.go (about) 1 package fingers 2 3 import ( 4 "bytes" 5 "fmt" 6 "os" 7 "path/filepath" 8 "runtime" 9 "sort" 10 "testing" 11 "time" 12 13 "github.com/chainreactors/fingers/common" 14 "github.com/chainreactors/fingers/resources" 15 ) 16 17 const frontendMHTMLPath = "../frontend.mhtml" 18 const chunkSize16KB = 16 * 1024 19 20 var benchmarkFrontendMatchCount int 21 22 func loadFrontendMHTML(tb testing.TB) []byte { 23 tb.Helper() 24 25 absPath, err := filepath.Abs(frontendMHTMLPath) 26 if err != nil { 27 tb.Fatalf("failed to resolve %s: %v", frontendMHTMLPath, err) 28 } 29 30 content, err := os.ReadFile(frontendMHTMLPath) 31 if err != nil { 32 if os.IsNotExist(err) { 33 tb.Skipf("test file not found: %s", absPath) 34 } 35 tb.Fatalf("failed to read %s: %v", absPath, err) 36 } 37 tb.Logf("loaded test file: %s (%d bytes)", absPath, len(content)) 38 return content 39 } 40 41 func newPerfEngine(tb testing.TB) *FingersEngine { 42 tb.Helper() 43 44 engine, err := NewFingersEngine(resources.FingersHTTPData, resources.FingersSocketData, resources.PortData) 45 if err != nil { 46 tb.Fatalf("failed to create fingers engine: %v", err) 47 } 48 return engine 49 } 50 51 func splitRawHTTP(content []byte) ([]byte, []byte, bool) { 52 sepIndex := bytes.Index(content, []byte("\r\n\r\n")) 53 if sepIndex == -1 { 54 return nil, content, false 55 } 56 bodyOffset := sepIndex + len("\r\n\r\n") 57 return content[:bodyOffset], content[bodyOffset:], true 58 } 59 60 func chunkedHTTPMatch(engine *FingersEngine, content []byte, chunkSize int) (common.Frameworks, common.Vulns, int) { 61 frames := make(common.Frameworks) 62 vulns := make(common.Vulns) 63 64 header, body, hasHTTPHeader := splitRawHTTP(content) 65 if len(body) == 0 { 66 partFrames, partVulns := engine.HTTPMatch(content, "") 67 frames.Merge(partFrames) 68 vulns.Merge(partVulns) 69 return frames, vulns, 1 70 } 71 72 chunks := 0 73 for offset := 0; offset < len(body); offset += chunkSize { 74 end := offset + chunkSize 75 if end > len(body) { 76 end = len(body) 77 } 78 79 part := body[offset:end] 80 chunkContent := part 81 if hasHTTPHeader { 82 chunkContent = make([]byte, len(header)+len(part)) 83 copy(chunkContent, header) 84 copy(chunkContent[len(header):], part) 85 } 86 87 partFrames, partVulns := engine.HTTPMatch(chunkContent, "") 88 frames.Merge(partFrames) 89 vulns.Merge(partVulns) 90 chunks++ 91 } 92 93 return frames, vulns, chunks 94 } 95 96 func frameworkNameDiff(left, right common.Frameworks) []string { 97 diff := make([]string, 0) 98 for name := range left { 99 if _, ok := right[name]; !ok { 100 diff = append(diff, name) 101 } 102 } 103 sort.Strings(diff) 104 return diff 105 } 106 107 func TestFingersEngine_FullMatchFrontendMHTMLCost(t *testing.T) { 108 engine := newPerfEngine(t) 109 content := loadFrontendMHTML(t) 110 111 start := time.Now() 112 frameworks, vulns := engine.HTTPMatch(content, "") 113 elapsed := time.Since(start) 114 115 t.Logf("single full HTTPMatch cost=%s, frameworks=%d, vulns=%d", elapsed, len(frameworks), len(vulns)) 116 } 117 118 func TestFingersEngine_Chunk16KFrontendMHTMLCost(t *testing.T) { 119 engine := newPerfEngine(t) 120 content := loadFrontendMHTML(t) 121 122 fullStart := time.Now() 123 fullFrames, fullVulns := engine.HTTPMatch(content, "") 124 fullElapsed := time.Since(fullStart) 125 126 chunkStart := time.Now() 127 chunkFrames, chunkVulns, chunkCount := chunkedHTTPMatch(engine, content, chunkSize16KB) 128 chunkElapsed := time.Since(chunkStart) 129 130 ratio := 0.0 131 if fullElapsed > 0 { 132 ratio = float64(chunkElapsed) / float64(fullElapsed) 133 } 134 135 fullOnly := frameworkNameDiff(fullFrames, chunkFrames) 136 chunkOnly := frameworkNameDiff(chunkFrames, fullFrames) 137 138 t.Logf("full HTTPMatch: cost=%s, frameworks=%d, vulns=%d", fullElapsed, len(fullFrames), len(fullVulns)) 139 t.Logf("chunked HTTPMatch(16KB): chunks=%d, cost=%s, frameworks=%d, vulns=%d, ratio_vs_full=%.2fx", chunkCount, chunkElapsed, len(chunkFrames), len(chunkVulns), ratio) 140 t.Logf("framework diff: full_only=%d, chunk_only=%d", len(fullOnly), len(chunkOnly)) 141 if len(fullOnly) > 0 && len(fullOnly) <= 20 { 142 t.Logf("full_only names: %v", fullOnly) 143 } 144 if len(chunkOnly) > 0 && len(chunkOnly) <= 20 { 145 t.Logf("chunk_only names: %v", chunkOnly) 146 } 147 } 148 149 func TestFingersEngine_ChunkSizeSweepFrontendMHTMLCost(t *testing.T) { 150 engine := newPerfEngine(t) 151 content := loadFrontendMHTML(t) 152 153 runtime.GC() 154 fullStart := time.Now() 155 fullFrames, fullVulns := engine.HTTPMatch(content, "") 156 fullElapsed := time.Since(fullStart) 157 158 t.Logf("baseline full HTTPMatch: cost=%s, frameworks=%d, vulns=%d", fullElapsed, len(fullFrames), len(fullVulns)) 159 160 chunkSizes := []int{ 161 16 * 1024, 162 32 * 1024, 163 64 * 1024, 164 128 * 1024, 165 256 * 1024, 166 } 167 168 for _, chunkSize := range chunkSizes { 169 runtime.GC() 170 start := time.Now() 171 frames, vulns, chunks := chunkedHTTPMatch(engine, content, chunkSize) 172 elapsed := time.Since(start) 173 174 ratio := 0.0 175 if fullElapsed > 0 { 176 ratio = float64(elapsed) / float64(fullElapsed) 177 } 178 179 fullOnly := frameworkNameDiff(fullFrames, frames) 180 chunkOnly := frameworkNameDiff(frames, fullFrames) 181 182 t.Logf( 183 "chunk=%dKB: chunks=%d, cost=%s, ratio_vs_full=%.2fx, frameworks=%d, vulns=%d, full_only=%d, chunk_only=%d", 184 chunkSize/1024, chunks, elapsed, ratio, len(frames), len(vulns), len(fullOnly), len(chunkOnly), 185 ) 186 } 187 } 188 189 func TestFingersEngine_SmallChunkSweepFrontendMHTMLCost(t *testing.T) { 190 engine := newPerfEngine(t) 191 content := loadFrontendMHTML(t) 192 193 runtime.GC() 194 fullStart := time.Now() 195 fullFrames, fullVulns := engine.HTTPMatch(content, "") 196 fullElapsed := time.Since(fullStart) 197 198 t.Logf("baseline full HTTPMatch: cost=%s, frameworks=%d, vulns=%d", fullElapsed, len(fullFrames), len(fullVulns)) 199 200 chunkSizes := []int{ 201 4 * 1024, 202 8 * 1024, 203 } 204 205 for _, chunkSize := range chunkSizes { 206 runtime.GC() 207 start := time.Now() 208 frames, vulns, chunks := chunkedHTTPMatch(engine, content, chunkSize) 209 elapsed := time.Since(start) 210 211 ratio := 0.0 212 if fullElapsed > 0 { 213 ratio = float64(elapsed) / float64(fullElapsed) 214 } 215 216 fullOnly := frameworkNameDiff(fullFrames, frames) 217 chunkOnly := frameworkNameDiff(frames, fullFrames) 218 219 t.Logf( 220 "chunk=%dKB: chunks=%d, cost=%s, ratio_vs_full=%.2fx, frameworks=%d, vulns=%d, full_only=%d, chunk_only=%d", 221 chunkSize/1024, chunks, elapsed, ratio, len(frames), len(vulns), len(fullOnly), len(chunkOnly), 222 ) 223 } 224 } 225 226 func BenchmarkFingersEngine_FullMatchFrontendMHTML(b *testing.B) { 227 engine := newPerfEngine(b) 228 content := loadFrontendMHTML(b) 229 230 b.ReportAllocs() 231 b.ResetTimer() 232 for i := 0; i < b.N; i++ { 233 frameworks, _ := engine.HTTPMatch(content, "") 234 benchmarkFrontendMatchCount = len(frameworks) 235 } 236 } 237 238 func BenchmarkFingersEngine_Chunk16KFrontendMHTML(b *testing.B) { 239 engine := newPerfEngine(b) 240 content := loadFrontendMHTML(b) 241 242 b.ReportAllocs() 243 b.ResetTimer() 244 for i := 0; i < b.N; i++ { 245 frameworks, _, _ := chunkedHTTPMatch(engine, content, chunkSize16KB) 246 benchmarkFrontendMatchCount = len(frameworks) 247 } 248 } 249 250 func BenchmarkFingersEngine_ChunkSizeSweepFrontendMHTML(b *testing.B) { 251 engine := newPerfEngine(b) 252 content := loadFrontendMHTML(b) 253 254 chunkSizes := []int{ 255 16 * 1024, 256 32 * 1024, 257 64 * 1024, 258 128 * 1024, 259 256 * 1024, 260 } 261 262 for _, chunkSize := range chunkSizes { 263 chunkSize := chunkSize 264 b.Run(fmt.Sprintf("%dKB", chunkSize/1024), func(b *testing.B) { 265 b.ReportAllocs() 266 b.ResetTimer() 267 for i := 0; i < b.N; i++ { 268 frameworks, _, _ := chunkedHTTPMatch(engine, content, chunkSize) 269 benchmarkFrontendMatchCount = len(frameworks) 270 } 271 }) 272 } 273 }