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  }