rack-smart_compress 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +30 -0
- data/README.md +198 -0
- data/lib/rack/smart_compress/configuration.rb +134 -0
- data/lib/rack/smart_compress/cpu_advisor.rb +74 -0
- data/lib/rack/smart_compress/encoders/base.rb +31 -0
- data/lib/rack/smart_compress/encoders/brotli.rb +33 -0
- data/lib/rack/smart_compress/encoders/deflate.rb +25 -0
- data/lib/rack/smart_compress/encoders/gzip.rb +31 -0
- data/lib/rack/smart_compress/encoders/zstd.rb +33 -0
- data/lib/rack/smart_compress/lru_cache.rb +87 -0
- data/lib/rack/smart_compress/middleware.rb +293 -0
- data/lib/rack/smart_compress/railtie.rb +35 -0
- data/lib/rack/smart_compress/static.rb +170 -0
- data/lib/rack/smart_compress/stream_body.rb +126 -0
- data/lib/rack/smart_compress/version.rb +7 -0
- data/lib/rack/smart_compress.rb +30 -0
- metadata +158 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 31b1ea77cbc0e6ff01ec184c4c99cc68c712ed24e94f8db1214f1cff24250ff8
|
|
4
|
+
data.tar.gz: b4fc25908f2e0b73872a7ca38a4276b41f75cc592a470f3144aa853063b2b8ca
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b7562181678286e9507ae01edbc8a01779c61ab0bfa97b94bde0768ddbf7d78bb8790090391b26355dae97ae4f3d25f4c0f61098367e7672ba2b552f303b85d2
|
|
7
|
+
data.tar.gz: a5ae38ec9123cc4d4350d55004c57128b89130254f1ccc9e831d0c61318065b131b55d22a95f42057bbedc4d5932b4afe489dc4d1c74f8a4369858d30160f3a3
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.2.0] - 2026-08-16
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Pre-Compressed Static Asset Middleware (`Rack::SmartCompress::Static`) supporting build-time `.zst`, `.br`, and `.gz` static files with 0% runtime CPU overhead.
|
|
12
|
+
- Configuration DSL via `Rack::SmartCompress.configure { |c| ... }` and `Rack::SmartCompress.configuration`.
|
|
13
|
+
- Dedicated Rails configuration hook via `config.smart_compress` namespace.
|
|
14
|
+
- RFC 9110 ยง8.8.1 ETag weakening (`W/"..."`) for dynamically compressed responses.
|
|
15
|
+
- RFC 9111 `Cache-Control: no-transform` support (skips compression when present).
|
|
16
|
+
- 206 Partial Content and `Content-Range` skipping to protect byte-range requests.
|
|
17
|
+
- Wildcard `Accept-Encoding: *` and explicit `q=0` rejection handling.
|
|
18
|
+
- Real chunked streaming for Brotli (`Brotli::Compressor`).
|
|
19
|
+
- ActiveSupport / Custom callable telemetry instrumentation (`rack_smart_compress.compress`).
|
|
20
|
+
- GitHub Actions CI workflow supporting Ruby 3.0, 3.1, 3.2, 3.3, and 3.4.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- Fixed Zstd streaming class typo (`Zstd::StreamingCompress` instead of non-existent `Zstd::StreamingCompressor`).
|
|
24
|
+
- Fixed `Rack::BodyProxy` streaming misclassification in Rails applications.
|
|
25
|
+
- Relieved LRU cache mutex contention by computing compression outside the lock.
|
|
26
|
+
- Optimized LRU cache key hashing to eliminate large memory allocations.
|
|
27
|
+
- Added 1-second load average sampling cooldown to prevent `/proc/loadavg` syscall overhead on high-frequency requests.
|
|
28
|
+
|
|
29
|
+
## [0.1.0] - Initial Release
|
|
30
|
+
- Initial release with Zstandard, Brotli, Gzip, and Deflate compression support.
|
data/README.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# โก `rack-smart_compress`
|
|
2
|
+
|
|
3
|
+
> High-performance **Zstandard (`zstd`)**, **Brotli (`br`)**, and **Gzip (`gzip`)** dynamic HTTP compression & pre-compressed static asset middleware for Rack and Ruby on Rails applications.
|
|
4
|
+
|
|
5
|
+
[](https://badge.fury.io/rb/rack-smart_compress)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
`Rack::SmartCompress` replaces traditional `Rack::Deflater` by automatically negotiating modern, hyper-efficient compression algorithms like **Zstd** and **Brotli** with fallback to Gzip. It applies smart payload thresholding, LRU caching, and content-type filtering to cut outgoing server bandwidth by up to **40%** with zero setup friction.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## ๐ฅ Why `Rack::SmartCompress` vs `Rack::Deflater`?
|
|
13
|
+
|
|
14
|
+
| Feature | `Rack::Deflater` | `rack-brotli` | `Rack::SmartCompress` โก |
|
|
15
|
+
| :--- | :---: | :---: | :---: |
|
|
16
|
+
| **Zstandard (`zstd`) Support** | โ | โ | โ
**Native** |
|
|
17
|
+
| **Brotli (`br`) Support** | โ | โ
| โ
**Native** |
|
|
18
|
+
| **Gzip & Deflate Fallback** | โ
| โ | โ
**Native** |
|
|
19
|
+
| **Chunked Stream Compression** | โ ๏ธ Buffers | โ | โ
**True Chunk Streaming** |
|
|
20
|
+
| **Pre-Compressed Static Assets (`.zst`, `.br`, `.gz`)** | โ | โ | โ
**0% CPU Overhead** |
|
|
21
|
+
| **RFC 9110 ETag Weakening (`W/"..."`)** | โ ๏ธ Partial | โ | โ
**RFC Compliant** |
|
|
22
|
+
| **`Cache-Control: no-transform` Respect** | โ | โ | โ
**RFC Compliant** |
|
|
23
|
+
| **206 Partial Content / Range Protection** | โ ๏ธ Buggy | โ | โ
**Protected** |
|
|
24
|
+
| **In-Memory LRU Payload Cache** | โ | โ | โ
**Thread-Safe LRU** |
|
|
25
|
+
| **Dynamic CPU Load Advisor** | โ | โ | โ
**Automatic** |
|
|
26
|
+
| **Rails Railtie Auto-Inject** | โ | โ | โ
**`config.smart_compress`** |
|
|
27
|
+
| **ActiveSupport Telemetry Instrumentation** | โ | โ | โ
**Built-in** |
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## โจ Features
|
|
32
|
+
|
|
33
|
+
- ๐๏ธ **Next-Gen Encodings:** Native support for **Zstandard (`zstd`)**, **Brotli (`br`)**, **Gzip**, and **Deflate**.
|
|
34
|
+
- ๐ฆ **Pre-Compressed Static Assets:** `Rack::SmartCompress::Static` serves build-time `.zst`, `.br`, and `.gz` files with **0% runtime CPU overhead**.
|
|
35
|
+
- ๐ **True Chunked Streaming:** Progressive, non-buffering stream compression for Zstandard (`Zstd::StreamingCompress`), Brotli (`Brotli::Compressor`), and Gzip.
|
|
36
|
+
- ๐ง **Smart Payload Filtering:** Automatically skips tiny payloads (< 1KB) where compression overhead increases latency without savings.
|
|
37
|
+
- ๐ก๏ธ **RFC 9110 & 9111 Compliant:** Automatic strong-to-weak ETag conversion (`W/"..."`), `Cache-Control: no-transform` respect, and 206 Partial Content / `Content-Range` protection.
|
|
38
|
+
- ๐ **Media & Binary Exclusion:** Skips already compressed assets (`images`, `audio`, `video`, `zip`, `pdf`).
|
|
39
|
+
- โก **Auto-Negotiation:** Respects HTTP `Accept-Encoding` quality weights (`qvalue`) and `*` wildcard sent by modern browsers and HTTP clients.
|
|
40
|
+
- ๐ฆ **Rails Auto-Integration:** Includes a built-in Railtie with `config.smart_compress` support.
|
|
41
|
+
- ๐ **Telemetry & Observability:** Emits `rack_smart_compress.compress` notifications via `ActiveSupport::Notifications` or custom callbacks.
|
|
42
|
+
- ๐ก๏ธ **Rack 2 & Rack 3 Compatible:** Fully compliant with modern Rack specifications.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## ๐ฆ Installation
|
|
47
|
+
|
|
48
|
+
Add this line to your application's `Gemfile`:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
gem "rack-smart_compress"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
To enable native **Brotli** and **Zstandard** compression drivers, add their gems to your Gemfile:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
gem "brotli", "~> 0.4" # Optional: enables Brotli (br) compression
|
|
58
|
+
gem "zstd-ruby", "~> 1.5" # Optional: enables Zstandard (zstd) compression
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Then execute:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
$ bundle install
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## ๐ Quick Start
|
|
70
|
+
|
|
71
|
+
### Ruby on Rails Integration
|
|
72
|
+
|
|
73
|
+
In Rails, `Rack::SmartCompress` automatically attaches to your middleware stack via Railtie.
|
|
74
|
+
|
|
75
|
+
Configure options in `config/initializers/smart_compress.rb` or `config/environments/production.rb`:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
Rails.application.configure do
|
|
79
|
+
config.smart_compress.min_size = 1024 # Only compress responses >= 1 KB
|
|
80
|
+
config.smart_compress.encodings = %w[zstd br gzip deflate]
|
|
81
|
+
config.smart_compress.cache = true # Enable in-memory LRU payload caching
|
|
82
|
+
config.smart_compress.cache_size = 200 # Cache up to 200 warm response payloads
|
|
83
|
+
config.smart_compress.dynamic_levels = true # Dynamically drop compression levels under high CPU load
|
|
84
|
+
config.smart_compress.if = ->(env, status, headers) { env["HTTP_X_NO_COMPRESS"].nil? }
|
|
85
|
+
|
|
86
|
+
# Optional: Enable pre-compressed static asset serving (.zst, .br, .gz)
|
|
87
|
+
config.smart_compress.static_assets = true
|
|
88
|
+
config.smart_compress.static_headers = { "Cache-Control" => "public, max-age=31536000, immutable" }
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Global Configuration (Sinatra / Hanami / Pure Rack)
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
# config.ru or boot.rb
|
|
96
|
+
require "rack/smart_compress"
|
|
97
|
+
|
|
98
|
+
Rack::SmartCompress.configure do |config|
|
|
99
|
+
config.min_size = 1024
|
|
100
|
+
config.cache = true
|
|
101
|
+
config.encodings = %w[zstd br gzip]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
use Rack::SmartCompress::Middleware
|
|
105
|
+
run YourRackApp.new
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Serving Pre-Compressed Static Assets (`.zst`, `.br`, `.gz`)
|
|
109
|
+
|
|
110
|
+
If your asset pipeline (Vite Ruby, Propshaft, Webpacker, esbuild) pre-compresses static assets into `.zst`, `.br`, and `.gz` files:
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
# config.ru
|
|
114
|
+
use Rack::SmartCompress::Static,
|
|
115
|
+
root: "public",
|
|
116
|
+
urls: ["/assets", "/packs"],
|
|
117
|
+
headers: { "Cache-Control" => "public, max-age=31536000, immutable" }
|
|
118
|
+
|
|
119
|
+
run YourRackApp.new
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
When a browser requests `/assets/bundle.js` with `Accept-Encoding: zstd, br, gzip`, `Rack::SmartCompress::Static` will:
|
|
123
|
+
1. Detect `bundle.js.zst` on disk.
|
|
124
|
+
2. Serve it directly via streaming IO with **0% runtime CPU usage**.
|
|
125
|
+
3. Set `Content-Type: application/javascript`, `Content-Encoding: zstd`, and `Vary: Accept-Encoding`.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## โ๏ธ Configuration Options
|
|
130
|
+
|
|
131
|
+
| Option | Default | Description |
|
|
132
|
+
| :--- | :--- | :--- |
|
|
133
|
+
| `min_size` | `1024` (1 KB) | Minimum response body size in bytes required to trigger compression. |
|
|
134
|
+
| `encodings` | `%w[zstd br gzip deflate]` | Priority order of compression algorithms to allow. |
|
|
135
|
+
| `cache` | `false` | Enable thread-safe in-memory LRU payload caching for warm endpoints. |
|
|
136
|
+
| `cache_size` | `200` | Maximum number of compressed payloads to cache in memory. |
|
|
137
|
+
| `dynamic_levels` | `false` | Automatically drops compression levels under high CPU load to preserve low latency. |
|
|
138
|
+
| `if` | `nil` | Proc/Lambda `->(env, status, headers)` returning true to compress. |
|
|
139
|
+
| `unless` | `nil` | Proc/Lambda `->(env, status, headers)` returning true to skip compression. |
|
|
140
|
+
| `mime_types` | `[text/*, application/json, ...]` | List of compressible MIME types. Supports `+json` and `+xml` suffixes. |
|
|
141
|
+
| `exclude_mime_types` | `[image/*, video/*, audio/*, zip, pdf]` | List of MIME type prefixes to explicitly skip. |
|
|
142
|
+
| `zstd_level` | `3` | Zstandard compression level (1-22). |
|
|
143
|
+
| `brotli_level` | `4` | Brotli compression quality level (0-11). |
|
|
144
|
+
| `instrumentation` | `true` | Emits `rack_smart_compress.compress` via `ActiveSupport::Notifications` or block callback. |
|
|
145
|
+
| `static_assets` | `false` | Enables pre-compressed static asset serving in Rails. |
|
|
146
|
+
| `static_root` | `"public"` | Root directory for static asset resolution. |
|
|
147
|
+
| `static_urls` | `["/"]` | URL prefixes intercepted by static pre-compressed asset middleware. |
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## ๐ Benchmark & Performance
|
|
152
|
+
|
|
153
|
+
Run the built-in benchmark script to compare performance against your payloads:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
$ bundle exec rake benchmark
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Sample Benchmark Output (108 KB JSON API payload):**
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
============================================================
|
|
163
|
+
Rack::SmartCompress Performance Benchmark
|
|
164
|
+
============================================================
|
|
165
|
+
Algorithm | Compressed Size | Compression % | Avg Speed (ms)
|
|
166
|
+
------------------------------------------------------------
|
|
167
|
+
Zstd | 3.07 KB | 97.16% | 0.085 ms
|
|
168
|
+
Brotli | 2.66 KB | 97.54% | 0.355 ms
|
|
169
|
+
Gzip | 8.16 KB | 92.45% | 0.498 ms
|
|
170
|
+
Deflate | 8.15 KB | 92.46% | 0.478 ms
|
|
171
|
+
============================================================
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
- **Zstandard (`zstd`)** delivers the fastest compression/decompression throughput (sub-0.1ms).
|
|
175
|
+
- **Brotli (`br`)** delivers the smallest payload size over the wire.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## ๐งช Running Tests
|
|
180
|
+
|
|
181
|
+
Run all unit and end-to-end integration tests:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
$ bundle exec rake
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Or individual suites:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
$ bundle exec rspec # RSpec unit tests (45 examples)
|
|
191
|
+
$ bundle exec rake e2e # Comprehensive End-to-End integration suite (18 tests)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## ๐ License
|
|
197
|
+
|
|
198
|
+
This gem is available as open source under the terms of the [MIT License](LICENSE.txt).
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
|
|
5
|
+
module Rack
|
|
6
|
+
module SmartCompress
|
|
7
|
+
class Configuration
|
|
8
|
+
DEFAULT_MIN_SIZE = 1024 # 1 KB
|
|
9
|
+
|
|
10
|
+
DEFAULT_ENCODINGS = %w[zstd br gzip deflate].freeze
|
|
11
|
+
|
|
12
|
+
DEFAULT_MIME_TYPES = [
|
|
13
|
+
"text/html",
|
|
14
|
+
"text/plain",
|
|
15
|
+
"text/css",
|
|
16
|
+
"text/javascript",
|
|
17
|
+
"text/xml",
|
|
18
|
+
"application/json",
|
|
19
|
+
"application/javascript",
|
|
20
|
+
"application/x-javascript",
|
|
21
|
+
"application/xml",
|
|
22
|
+
"application/xhtml+xml",
|
|
23
|
+
"application/rss+xml",
|
|
24
|
+
"application/atom+xml",
|
|
25
|
+
"application/wasm",
|
|
26
|
+
"image/svg+xml"
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
DEFAULT_EXCLUDED_MIME_TYPES = [
|
|
30
|
+
"image/jpeg",
|
|
31
|
+
"image/png",
|
|
32
|
+
"image/gif",
|
|
33
|
+
"image/webp",
|
|
34
|
+
"image/avif",
|
|
35
|
+
"audio/",
|
|
36
|
+
"video/",
|
|
37
|
+
"application/zip",
|
|
38
|
+
"application/x-gzip",
|
|
39
|
+
"application/pdf"
|
|
40
|
+
].freeze
|
|
41
|
+
|
|
42
|
+
attr_accessor :min_size,
|
|
43
|
+
:encodings,
|
|
44
|
+
:mime_types,
|
|
45
|
+
:exclude_mime_types,
|
|
46
|
+
:cache,
|
|
47
|
+
:cache_size,
|
|
48
|
+
:dynamic_levels,
|
|
49
|
+
:zstd_level,
|
|
50
|
+
:brotli_level,
|
|
51
|
+
:gzip_level,
|
|
52
|
+
:deflate_level,
|
|
53
|
+
:if_condition,
|
|
54
|
+
:unless_condition,
|
|
55
|
+
:instrumentation,
|
|
56
|
+
:static_assets,
|
|
57
|
+
:static_root,
|
|
58
|
+
:static_urls,
|
|
59
|
+
:static_headers,
|
|
60
|
+
:static_cascade
|
|
61
|
+
|
|
62
|
+
def initialize
|
|
63
|
+
@min_size = DEFAULT_MIN_SIZE
|
|
64
|
+
@encodings = DEFAULT_ENCODINGS.dup
|
|
65
|
+
@mime_types = DEFAULT_MIME_TYPES.dup
|
|
66
|
+
@exclude_mime_types = DEFAULT_EXCLUDED_MIME_TYPES.dup
|
|
67
|
+
@cache = false
|
|
68
|
+
@cache_size = 200
|
|
69
|
+
@dynamic_levels = false
|
|
70
|
+
@zstd_level = 3
|
|
71
|
+
@brotli_level = 4
|
|
72
|
+
@gzip_level = Zlib::DEFAULT_COMPRESSION
|
|
73
|
+
@deflate_level = Zlib::DEFAULT_COMPRESSION
|
|
74
|
+
@if_condition = nil
|
|
75
|
+
@unless_condition = nil
|
|
76
|
+
@instrumentation = true
|
|
77
|
+
@static_assets = false
|
|
78
|
+
@static_root = "public"
|
|
79
|
+
@static_urls = ["/"]
|
|
80
|
+
@static_headers = {}
|
|
81
|
+
@static_cascade = true
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Convenience aliases for :if and :unless
|
|
85
|
+
def if(&block)
|
|
86
|
+
if block_given?
|
|
87
|
+
@if_condition = block
|
|
88
|
+
else
|
|
89
|
+
@if_condition
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def if=(val)
|
|
94
|
+
@if_condition = val
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def unless(&block)
|
|
98
|
+
if block_given?
|
|
99
|
+
@unless_condition = block
|
|
100
|
+
else
|
|
101
|
+
@unless_condition
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def unless=(val)
|
|
106
|
+
@unless_condition = val
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def to_h
|
|
110
|
+
{
|
|
111
|
+
min_size: @min_size,
|
|
112
|
+
encodings: @encodings,
|
|
113
|
+
mime_types: @mime_types,
|
|
114
|
+
exclude_mime_types: @exclude_mime_types,
|
|
115
|
+
cache: @cache,
|
|
116
|
+
cache_size: @cache_size,
|
|
117
|
+
dynamic_levels: @dynamic_levels,
|
|
118
|
+
zstd_level: @zstd_level,
|
|
119
|
+
brotli_level: @brotli_level,
|
|
120
|
+
gzip_level: @gzip_level,
|
|
121
|
+
deflate_level: @deflate_level,
|
|
122
|
+
if: @if_condition,
|
|
123
|
+
unless: @unless_condition,
|
|
124
|
+
instrumentation: @instrumentation,
|
|
125
|
+
static_assets: @static_assets,
|
|
126
|
+
static_root: @static_root,
|
|
127
|
+
static_urls: @static_urls,
|
|
128
|
+
static_headers: @static_headers,
|
|
129
|
+
static_cascade: @static_cascade
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "etc"
|
|
4
|
+
require "zlib"
|
|
5
|
+
|
|
6
|
+
module Rack
|
|
7
|
+
module SmartCompress
|
|
8
|
+
class CpuAdvisor
|
|
9
|
+
SAMPLE_INTERVAL = 1.0 # Sample at most once per second
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def high_load?
|
|
13
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
14
|
+
if @last_check.nil? || (now - @last_check) >= SAMPLE_INTERVAL
|
|
15
|
+
@last_check = now
|
|
16
|
+
@cached_high_load = compute_high_load
|
|
17
|
+
end
|
|
18
|
+
@cached_high_load
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def ncpu_count
|
|
22
|
+
@ncpu_count ||= begin
|
|
23
|
+
Etc.nprocessors
|
|
24
|
+
rescue StandardError
|
|
25
|
+
2
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def adjusted_level(encoder_name, default_level)
|
|
30
|
+
return default_level unless high_load?
|
|
31
|
+
|
|
32
|
+
case encoder_name
|
|
33
|
+
when "zstd"
|
|
34
|
+
1 # Fastest Zstd level
|
|
35
|
+
when "br"
|
|
36
|
+
1 # Fastest Brotli quality level
|
|
37
|
+
when "gzip", "deflate"
|
|
38
|
+
Zlib::BEST_SPEED # Fastest Gzip level (1)
|
|
39
|
+
else
|
|
40
|
+
default_level
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def reset!
|
|
45
|
+
@last_check = nil
|
|
46
|
+
@cached_high_load = nil
|
|
47
|
+
@ncpu_count = nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def compute_high_load
|
|
53
|
+
load_1min = current_load_avg
|
|
54
|
+
return false if load_1min.nil?
|
|
55
|
+
|
|
56
|
+
ncpu = ncpu_count
|
|
57
|
+
load_1min > (ncpu * 0.85) # High load threshold (> 85% capacity)
|
|
58
|
+
rescue StandardError
|
|
59
|
+
false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def current_load_avg
|
|
63
|
+
if File.exist?("/proc/loadavg")
|
|
64
|
+
File.read("/proc/loadavg").split.first.to_f
|
|
65
|
+
else
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
rescue StandardError
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rack
|
|
4
|
+
module SmartCompress
|
|
5
|
+
module Encoders
|
|
6
|
+
class Base
|
|
7
|
+
class << self
|
|
8
|
+
def available?
|
|
9
|
+
true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def compress(body, level: nil)
|
|
13
|
+
raise NotImplementedError, "#{name}.compress must be implemented"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def encode_body(body, level: nil)
|
|
17
|
+
compressed = []
|
|
18
|
+
if body.respond_to?(:each)
|
|
19
|
+
buffer = String.new
|
|
20
|
+
body.each { |chunk| buffer << chunk.to_s }
|
|
21
|
+
compressed << compress(buffer, level: level)
|
|
22
|
+
else
|
|
23
|
+
compressed << compress(body.to_s, level: level)
|
|
24
|
+
end
|
|
25
|
+
compressed
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Rack
|
|
6
|
+
module SmartCompress
|
|
7
|
+
module Encoders
|
|
8
|
+
class Brotli < Base
|
|
9
|
+
ENCODING_NAME = "br"
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def available?
|
|
13
|
+
return @available if defined?(@available)
|
|
14
|
+
|
|
15
|
+
@available = begin
|
|
16
|
+
require "brotli"
|
|
17
|
+
true
|
|
18
|
+
rescue LoadError
|
|
19
|
+
false
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def compress(content, level: nil)
|
|
24
|
+
raise LoadError, "brotli gem is not available" unless available?
|
|
25
|
+
|
|
26
|
+
compression_level = level || 4
|
|
27
|
+
::Brotli.deflate(content, quality: compression_level)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
require_relative "base"
|
|
5
|
+
|
|
6
|
+
module Rack
|
|
7
|
+
module SmartCompress
|
|
8
|
+
module Encoders
|
|
9
|
+
class Deflate < Base
|
|
10
|
+
ENCODING_NAME = "deflate"
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def available?
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def compress(content, level: nil)
|
|
18
|
+
compression_level = level || Zlib::DEFAULT_COMPRESSION
|
|
19
|
+
Zlib::Deflate.deflate(content, compression_level)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
require "stringio"
|
|
5
|
+
require_relative "base"
|
|
6
|
+
|
|
7
|
+
module Rack
|
|
8
|
+
module SmartCompress
|
|
9
|
+
module Encoders
|
|
10
|
+
class Gzip < Base
|
|
11
|
+
ENCODING_NAME = "gzip"
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def available?
|
|
15
|
+
true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def compress(content, level: nil)
|
|
19
|
+
compression_level = level || Zlib::DEFAULT_COMPRESSION
|
|
20
|
+
io = StringIO.new
|
|
21
|
+
io.set_encoding(Encoding::BINARY)
|
|
22
|
+
writer = Zlib::GzipWriter.new(io, compression_level)
|
|
23
|
+
writer.write(content)
|
|
24
|
+
writer.close
|
|
25
|
+
io.string
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Rack
|
|
6
|
+
module SmartCompress
|
|
7
|
+
module Encoders
|
|
8
|
+
class Zstd < Base
|
|
9
|
+
ENCODING_NAME = "zstd"
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def available?
|
|
13
|
+
return @available if defined?(@available)
|
|
14
|
+
|
|
15
|
+
@available = begin
|
|
16
|
+
require "zstd-ruby"
|
|
17
|
+
true
|
|
18
|
+
rescue LoadError
|
|
19
|
+
false
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def compress(content, level: nil)
|
|
24
|
+
raise LoadError, "zstd-ruby gem is not available" unless available?
|
|
25
|
+
|
|
26
|
+
compression_level = level || 3
|
|
27
|
+
::Zstd.compress(content, level: compression_level)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "monitor"
|
|
5
|
+
|
|
6
|
+
module Rack
|
|
7
|
+
module SmartCompress
|
|
8
|
+
class LruCache
|
|
9
|
+
include MonitorMixin
|
|
10
|
+
|
|
11
|
+
DEFAULT_MAX_SIZE = 200
|
|
12
|
+
|
|
13
|
+
attr_reader :max_size, :hits, :misses
|
|
14
|
+
|
|
15
|
+
def initialize(max_size = DEFAULT_MAX_SIZE)
|
|
16
|
+
super()
|
|
17
|
+
@max_size = max_size
|
|
18
|
+
@cache = {}
|
|
19
|
+
@hits = 0
|
|
20
|
+
@misses = 0
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def get(key)
|
|
24
|
+
mon_synchronize do
|
|
25
|
+
if @cache.key?(key)
|
|
26
|
+
@hits += 1
|
|
27
|
+
value = @cache.delete(key)
|
|
28
|
+
@cache[key] = value # Move to end (MRU)
|
|
29
|
+
value
|
|
30
|
+
else
|
|
31
|
+
@misses += 1
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def fetch(key)
|
|
38
|
+
cached = get(key)
|
|
39
|
+
return cached unless cached.nil?
|
|
40
|
+
return nil unless block_given?
|
|
41
|
+
|
|
42
|
+
# Execute expensive compression outside the mutex lock
|
|
43
|
+
value = yield
|
|
44
|
+
put(key, value)
|
|
45
|
+
value
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def put(key, value)
|
|
49
|
+
mon_synchronize do
|
|
50
|
+
@cache.delete(key)
|
|
51
|
+
@cache[key] = value
|
|
52
|
+
|
|
53
|
+
if @cache.size > @max_size
|
|
54
|
+
@cache.shift # Remove oldest (LRU)
|
|
55
|
+
end
|
|
56
|
+
value
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def delete(key)
|
|
61
|
+
mon_synchronize { @cache.delete(key) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def clear
|
|
65
|
+
mon_synchronize do
|
|
66
|
+
@cache.clear
|
|
67
|
+
@hits = 0
|
|
68
|
+
@misses = 0
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def size
|
|
73
|
+
mon_synchronize { @cache.size }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def build_key(encoder_name, level, content)
|
|
77
|
+
digest = Digest::SHA256.new
|
|
78
|
+
digest.update(encoder_name.to_s)
|
|
79
|
+
digest.update(":")
|
|
80
|
+
digest.update(level.to_s)
|
|
81
|
+
digest.update(":")
|
|
82
|
+
digest.update(content.to_s)
|
|
83
|
+
digest.hexdigest
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|