Skip to content

refactor: track bundle size and gzip size in raw bytes #259

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

z0ffy
Copy link
Owner

@z0ffy z0ffy commented Aug 14, 2025

  • Add originalBytes and obfuscatedBytes to store raw byte values.
  • Update calculateBundleSize to return both formatted sizes and raw byte data.
  • Improve percentage increase calculations using raw byte values for accuracy.

Summary by Sourcery

Track raw total and gzip byte sizes in the code size analyzer, update bundle size calculations to return formatted and raw data, improve percentage increase accuracy, and add tests for unit variations and zero-size scenarios

New Features:

  • Track total and gzip bundle sizes in raw bytes alongside formatted values
  • Extend calculateBundleSize to return both formatted size results and raw byte data

Bug Fixes:

  • Prevent division-by-zero in percentage calculations when original size is zero

Enhancements:

  • Compute percentage increases using raw byte values for more accurate results

Tests:

  • Add tests for percentage calculation with differing units
  • Add tests to verify zero-division handling in percentage calculations

- Add `originalBytes` and `obfuscatedBytes` to store raw byte values.
- Update `calculateBundleSize` to return both formatted sizes and raw byte data.
- Improve percentage increase calculations using raw byte values for accuracy.
Copy link

sourcery-ai bot commented Aug 14, 2025

Reviewer's Guide

The PR refactors CodeSizeAnalyzer to capture raw byte counts alongside formatted sizes, updates calculateBundleSize to return both formats, switches percentage computations to rely on raw bytes with zero-division safeguards, and adds tests for unit variance and empty bundles.

File-Level Changes

Change Details Files
Track raw byte values for bundles
  • Introduce originalBytes and obfuscatedBytes properties
  • Initialize raw byte properties in the constructor
  • Destructure and assign bytes from calculateBundleSize in start/end
src/utils/index.ts
Extend calculateBundleSize to return formatted sizes and raw bytes
  • Change return signature to include size and bytes objects
  • Populate bytes.total and bytes.gzip alongside formatted size
src/utils/index.ts
Refine percentage increase logic using raw bytes
  • Replace formatted-value diff with raw-byte diff calculations
  • Add zero-division guards for total and gzip percentages
src/utils/index.ts
Augment tests for new behavior
  • Add test for correct percentage when units differ
  • Add test to ensure zero-division yields 0.00%
src/__tests__/plugin.spec.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments

### Comment 1
<location> `src/utils/index.ts:267` </location>
<code_context>
   private _log;
   private originalSize: SizeResult;
   private obfuscatedSize: SizeResult;
+  private originalBytes: { total: number; gzip: number };
+  private obfuscatedBytes: { total: number; gzip: number };
   private startTime: number;
   private endTime: number;
</code_context>

<issue_to_address>
Consider using a type alias for byte size objects.

Defining a type alias for the shared structure will make future updates easier and the code more readable.

Suggested implementation:

```typescript
type ByteSize = { total: number; gzip: number };

  private _log;
  private originalSize: SizeResult;
  private obfuscatedSize: SizeResult;
  private originalBytes: ByteSize;
  private obfuscatedBytes: ByteSize;
  private startTime: number;
  private endTime: number;

```

```typescript
    this._log = log;
    this.originalSize = this.createEmptySizeResult();
    this.obfuscatedSize = this.createEmptySizeResult();
    this.originalBytes = { total: 0, gzip: 0 };
    this.obfuscatedBytes = { total: 0, gzip: 0 };
    this.startTime = 0;
    this.endTime = 0;
  }

```
</issue_to_address>

### Comment 2
<location> `src/__tests__/plugin.spec.ts:301` </location>
<code_context>
     expect(lastCallArgs[1]).toContain('MB');
   });
+
+  it('should calculate percentage correctly when units differ', () => {
+    const originalBundle: BundleList = [
+      ['test.js', { code: 'a'.repeat(500 * 1024) } as any]
+    ];
+
+    const obfuscatedBundle: BundleList = [
+      ['test.js', { code: 'a'.repeat(2 * 1024 * 1024) } as any]
+    ];
+
+    analyzer.start(originalBundle);
+    analyzer.end(obfuscatedBundle);
+
+    const lastCallArgs = logSpy.mock.lastCall;
+    expect(lastCallArgs[0]).toBe('\x1b[32m%s\x1b[0m');
+    const result = lastCallArgs[1];
+    
+    expect(result).toMatch(/\d+(\.\d+)?KB.*→.*\d+(\.\d+)?MB/);
+    expect(result).toMatch(/309\.\d+%/);
+  });
+
</code_context>

<issue_to_address>
Consider adding assertions for gzip percentage increase and output format.

Adding assertions for gzip percentage and its output format will help verify that both raw and gzip metrics are correctly calculated and displayed, aligning with the updated implementation.
</issue_to_address>

<suggested_fix>
<<<<<<< SEARCH
    expect(result).toMatch(/\d+(\.\d+)?KB.*→.*\d+(\.\d+)?MB/);
    expect(result).toMatch(/309\.\d+%/);
  });
=======
    expect(result).toMatch(/\d+(\.\d+)?KB.*→.*\d+(\.\d+)?MB/);
    expect(result).toMatch(/309\.\d+%/);

    // Assert gzip output format (e.g., KB → MB)
    expect(result).toMatch(/gzip:\s*\d+(\.\d+)?KB.*→.*\d+(\.\d+)?MB/);

    // Assert gzip percentage increase (should be similar to raw, but calculated separately)
    // The actual percentage may differ depending on gzip implementation, so match a percentage pattern
    expect(result).toMatch(/gzip:.*\(\+\d+(\.\d+)?%\)/);
  });
>>>>>>> REPLACE

</suggested_fix>

### Comment 3
<location> `src/__tests__/plugin.spec.ts:310` </location>
<code_context>
+      ['test.js', { code: 'a'.repeat(2 * 1024 * 1024) } as any]
+    ];
+
+    analyzer.start(originalBundle);
+    analyzer.end(obfuscatedBundle);
+
+    const lastCallArgs = logSpy.mock.lastCall;
</code_context>

<issue_to_address>
Missing test for negative percentage increase (obfuscated smaller than original).

Please add a test where the obfuscated bundle is smaller than the original to ensure negative percentage increases are handled and displayed correctly.
</issue_to_address>

### Comment 4
<location> `src/utils/index.ts:304` </location>
<code_context>
   }

-  private calculateBundleSize(bundleList: BundleList): { original: FormatSizeResult; gzip: FormatSizeResult } {
+  private calculateBundleSize(bundleList: BundleList): {
+    size: { original: FormatSizeResult; gzip: FormatSizeResult };
+    bytes: { total: number; gzip: number };
</code_context>

<issue_to_address>
Consider simplifying the code by returning and storing only raw byte sizes, and formatting sizes on-the-fly in the logging method.

```suggestion
// 1. Simplify calculateBundleSize to return raw bytes only
private calculateBundleSize(bundleList: BundleList): { total: number; gzip: number } {
  const { totalSize, gzipSize } = bundleList.reduce(
    (acc, [, bundleItem]) => {
      if (bundleItem.code) {
        const code = bundleItem.code;
        acc.totalSize += Buffer.byteLength(code, 'utf-8');
        acc.gzipSize  += gzipSync(code, { level: 9 }).byteLength;
      }
      return acc;
    },
    { totalSize: 0, gzipSize: 0 },
  );

  return { total: totalSize, gzip: gzipSize };
}

// 2. Drop originalSize/obfuscatedSize fields and only keep raw bytes
private originalBytes = { total: 0, gzip: 0 };
private obfuscatedBytes = { total: 0, gzip: 0 };

start(originalBundleList: BundleList): void {
  this.startTime = performance.now();
  this.originalBytes = this.calculateBundleSize(originalBundleList);
}

end(obfuscatedBundleList: BundleList): void {
  this.obfuscatedBytes = this.calculateBundleSize(obfuscatedBundleList);
  this.endTime = performance.now();
  this.logResult();
}

// 3. Format sizes on-the-fly in analyze(), removing nested destructuring and guard logic
private analyze(): string {
  const { total: oT, gzip: oG } = this.originalBytes;
  const { total: fT, gzip: fG } = this.obfuscatedBytes;

  const origSize  = formatSize(oT);
  const origGzip  = formatSize(oG);
  const newSize   = formatSize(fT);
  const newGzip   = formatSize(fG);

  const pct       = oT  > 0 ? (((fT - oT) / oT) * 100).toFixed(2) : '0.00';
  const gzipPct   = oG > 0 ? (((fG - oG) / oG) * 100).toFixed(2) : '0.00';
  const timeTaken = formatTime(this.endTime - this.startTime);

  return `✓ obfuscated in ${timeTaken} | `
    + `📦 ${origSize.value}${origSize.unit} (gzip: ${origGzip.value}${origGzip.unit}) → `
    + `🔒 ${newSize.value}${newSize.unit} (gzip: ${newGzip.value}${newGzip.unit}) | `
    + `📈 ${pct}% (gzip: ${gzipPct}%)`;
}
```

This keeps all functionality but removes the extra `size` object, nested destructuring, and boilerplate guard logic, by formatting sizes right before logging.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +267 to +268
private originalBytes: { total: number; gzip: number };
private obfuscatedBytes: { total: number; gzip: number };
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider using a type alias for byte size objects.

Defining a type alias for the shared structure will make future updates easier and the code more readable.

Suggested implementation:

type ByteSize = { total: number; gzip: number };

  private _log;
  private originalSize: SizeResult;
  private obfuscatedSize: SizeResult;
  private originalBytes: ByteSize;
  private obfuscatedBytes: ByteSize;
  private startTime: number;
  private endTime: number;
    this._log = log;
    this.originalSize = this.createEmptySizeResult();
    this.obfuscatedSize = this.createEmptySizeResult();
    this.originalBytes = { total: 0, gzip: 0 };
    this.obfuscatedBytes = { total: 0, gzip: 0 };
    this.startTime = 0;
    this.endTime = 0;
  }

Comment on lines +317 to +319
expect(result).toMatch(/\d+(\.\d+)?KB.*→.*\d+(\.\d+)?MB/);
expect(result).toMatch(/309\.\d+%/);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Consider adding assertions for gzip percentage increase and output format.

Adding assertions for gzip percentage and its output format will help verify that both raw and gzip metrics are correctly calculated and displayed, aligning with the updated implementation.

Suggested change
expect(result).toMatch(/\d+(\.\d+)?KB.*.*\d+(\.\d+)?MB/);
expect(result).toMatch(/309\.\d+%/);
});
expect(result).toMatch(/\d+(\.\d+)?KB.*.*\d+(\.\d+)?MB/);
expect(result).toMatch(/309\.\d+%/);
// Assert gzip output format (e.g., KB → MB)
expect(result).toMatch(/gzip:\s*\d+(\.\d+)?KB.*.*\d+(\.\d+)?MB/);
// Assert gzip percentage increase (should be similar to raw, but calculated separately)
// The actual percentage may differ depending on gzip implementation, so match a percentage pattern
expect(result).toMatch(/gzip:.*\(\+\d+(\.\d+)?%\)/);
});

Comment on lines +310 to +311
analyzer.start(originalBundle);
analyzer.end(obfuscatedBundle);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Missing test for negative percentage increase (obfuscated smaller than original).

Please add a test where the obfuscated bundle is smaller than the original to ensure negative percentage increases are handled and displayed correctly.

this.endTime = performance.now();
this.logResult();
}

private calculateBundleSize(bundleList: BundleList): { original: FormatSizeResult; gzip: FormatSizeResult } {
private calculateBundleSize(bundleList: BundleList): {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider simplifying the code by returning and storing only raw byte sizes, and formatting sizes on-the-fly in the logging method.

Suggested change
private calculateBundleSize(bundleList: BundleList): {
// 1. Simplify calculateBundleSize to return raw bytes only
private calculateBundleSize(bundleList: BundleList): { total: number; gzip: number } {
const { totalSize, gzipSize } = bundleList.reduce(
(acc, [, bundleItem]) => {
if (bundleItem.code) {
const code = bundleItem.code;
acc.totalSize += Buffer.byteLength(code, 'utf-8');
acc.gzipSize += gzipSync(code, { level: 9 }).byteLength;
}
return acc;
},
{ totalSize: 0, gzipSize: 0 },
);
return { total: totalSize, gzip: gzipSize };
}
// 2. Drop originalSize/obfuscatedSize fields and only keep raw bytes
private originalBytes = { total: 0, gzip: 0 };
private obfuscatedBytes = { total: 0, gzip: 0 };
start(originalBundleList: BundleList): void {
this.startTime = performance.now();
this.originalBytes = this.calculateBundleSize(originalBundleList);
}
end(obfuscatedBundleList: BundleList): void {
this.obfuscatedBytes = this.calculateBundleSize(obfuscatedBundleList);
this.endTime = performance.now();
this.logResult();
}
// 3. Format sizes on-the-fly in analyze(), removing nested destructuring and guard logic
private analyze(): string {
const { total: oT, gzip: oG } = this.originalBytes;
const { total: fT, gzip: fG } = this.obfuscatedBytes;
const origSize = formatSize(oT);
const origGzip = formatSize(oG);
const newSize = formatSize(fT);
const newGzip = formatSize(fG);
const pct = oT > 0 ? (((fT - oT) / oT) * 100).toFixed(2) : '0.00';
const gzipPct = oG > 0 ? (((fG - oG) / oG) * 100).toFixed(2) : '0.00';
const timeTaken = formatTime(this.endTime - this.startTime);
return `✓ obfuscated in ${timeTaken} | `
+ `📦 ${origSize.value}${origSize.unit} (gzip: ${origGzip.value}${origGzip.unit}) → `
+ `🔒 ${newSize.value}${newSize.unit} (gzip: ${newGzip.value}${newGzip.unit}) | `
+ `📈 ${pct}% (gzip: ${gzipPct}%)`;
}

This keeps all functionality but removes the extra size object, nested destructuring, and boilerplate guard logic, by formatting sizes right before logging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant