Skip to content
This repository was archived by the owner on Jan 29, 2020. It is now read-only.

Commit 620dd9a

Browse files
committed
Merge branch 'fix/#223-#224-high-memory-usage-in-sapi-stream-emitter' into develop
Forward port #223 Forward port #224
2 parents 972b0e1 + f9f3afe commit 620dd9a

File tree

2 files changed

+239
-21
lines changed

2 files changed

+239
-21
lines changed

src/Response/SapiStreamEmitter.php

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,14 @@ private function emitBody(ResponseInterface $response, $maxBufferLength)
5858
{
5959
$body = $response->getBody();
6060

61-
if (! $body->isSeekable()) {
61+
if ($body->isSeekable()) {
62+
$body->rewind();
63+
}
64+
65+
if (! $body->isReadable()) {
6266
echo $body;
63-
return;
6467
}
6568

66-
$body->rewind();
6769
while (! $body->eof()) {
6870
echo $body->read($maxBufferLength);
6971
}
@@ -82,24 +84,29 @@ private function emitBodyRange(array $range, ResponseInterface $response, $maxBu
8284

8385
$body = $response->getBody();
8486

85-
if (! $body->isSeekable()) {
86-
$contents = $body->getContents();
87-
echo substr($contents, $first, $last - $first + 1);
88-
return;
87+
$length = $last - $first + 1;
88+
89+
if ($body->isSeekable()) {
90+
$body->seek($first);
91+
92+
$first = 0;
8993
}
9094

91-
$body = new RelativeStream($body, $first);
92-
$body->rewind();
93-
$pos = 0;
94-
$length = $last - $first + 1;
95-
while (! $body->eof() && $pos < $length) {
96-
if (($pos + $maxBufferLength) > $length) {
97-
echo $body->read($length - $pos);
98-
break;
99-
}
95+
if (! $body->isReadable()) {
96+
echo substr($body->getContents(), $first, $length);
97+
}
10098

101-
echo $body->read($maxBufferLength);
102-
$pos = $body->tell();
99+
$remaining = $length;
100+
101+
while ($remaining >= $maxBufferLength && ! $body->eof()) {
102+
$contents = $body->read($maxBufferLength);
103+
$remaining -= strlen($contents);
104+
105+
echo $contents;
106+
}
107+
108+
if ($remaining > 0 && ! $body->eof()) {
109+
echo $body->read($remaining);
103110
}
104111
}
105112

test/Response/SapiStreamEmitterTest.php

Lines changed: 214 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@
99

1010
namespace ZendTest\Diactoros\Response;
1111

12-
use PHPUnit_Framework_TestCase as TestCase;
13-
use Psr\Http\Message\ResponseInterface;
14-
use Psr\Http\Message\StreamInterface;
12+
use Prophecy\Argument;
1513
use Zend\Diactoros\CallbackStream;
1614
use Zend\Diactoros\Response;
1715
use Zend\Diactoros\Response\SapiStreamEmitter;
@@ -43,6 +41,7 @@ public function testDoesNotInjectContentLengthHeaderIfStreamSizeIsUnknown()
4341
$stream = $this->prophesize('Psr\Http\Message\StreamInterface');
4442
$stream->__toString()->willReturn('Content!');
4543
$stream->isSeekable()->willReturn(false);
44+
$stream->isReadable()->willReturn(false);
4645
$stream->eof()->willReturn(true);
4746
$stream->rewind()->willReturn(true);
4847
$stream->getSize()->willReturn(null);
@@ -58,6 +57,218 @@ public function testDoesNotInjectContentLengthHeaderIfStreamSizeIsUnknown()
5857
}
5958
}
6059

60+
public function emitBodyProvider()
61+
{
62+
return [
63+
[true, '01234567890123456789' , 10, 2],
64+
[true, '012345678901234567890123', 10, 3],
65+
[false, '01234567890123456789' , 10, 2],
66+
[false, '012345678901234567890123', 10, 3],
67+
];
68+
}
69+
70+
/**
71+
* @dataProvider emitBodyProvider
72+
*/
73+
public function testEmitBody($seekable, $contents, $maxBufferLength, $expectedReads = 0)
74+
{
75+
$position = 0;
76+
77+
$stream = $this->prophesize('Psr\Http\Message\StreamInterface');
78+
$stream->getSize()->willReturn(strlen($contents));
79+
$stream->isSeekable()->willReturn($seekable);
80+
$stream->isReadable()->willReturn(true);
81+
$stream->__toString()->willReturn($contents);
82+
$stream->getContents()->willReturn($contents);
83+
$stream->rewind()->willReturn(true);
84+
85+
$stream->eof()->will(function () use (& $contents, & $position) {
86+
return ! isset($contents[$position]);
87+
});
88+
89+
$stream->read(Argument::type('integer'))->will(function ($args) use (& $contents, & $position) {
90+
$data = substr($contents, $position, $args[0]);
91+
$position += strlen($data);
92+
93+
return $data;
94+
});
95+
96+
$response = (new Response())
97+
->withStatus(200)
98+
->withBody($stream->reveal());
99+
100+
ob_start();
101+
$this->emitter->emit($response, $maxBufferLength);
102+
$stream->rewind()->shouldBeCalledTimes($seekable ? 1 : 0);
103+
$stream->read(Argument::type('integer'))->shouldBeCalledTimes($expectedReads);
104+
$this->assertEquals($contents, ob_get_clean());
105+
}
106+
107+
public function emitMemoryUsageProvider()
108+
{
109+
return [
110+
[true, 512, 1000, 20, null],
111+
[true, 8192, 1000, 20, null],
112+
[false, 512, 1000, 20, null],
113+
[false, 8192, 1000, 20, null],
114+
[true, 512, 1000, 20, [25, 75]],
115+
[true, 8192, 1000, 20, [25, 75]],
116+
[true, 512, 1000, 20, [250, 750]],
117+
[true, 8192, 1000, 20, [250, 750]],
118+
[false, 512, 1000, 20, [25, 75]],
119+
[false, 8192, 1000, 20, [25, 75]],
120+
[false, 512, 1000, 20, [250, 750]],
121+
[false, 8192, 1000, 20, [250, 750]],
122+
];
123+
}
124+
125+
/**
126+
* @dataProvider emitMemoryUsageProvider
127+
*/
128+
public function testEmitMemoryUsage($seekable, $maxBufferLength, $sizeBlocks, $maxAllowedBlocks, $rangeBlocks)
129+
{
130+
$sizeBytes = $maxBufferLength * $sizeBlocks;
131+
$maxAllowedMemoryUsage = $maxBufferLength * $maxAllowedBlocks;
132+
$peakBufferLength = 0;
133+
$peakMemoryUsage = 0;
134+
135+
$position = 0;
136+
137+
if ($rangeBlocks) {
138+
$first = $maxBufferLength * $rangeBlocks[0];
139+
$last = $maxBufferLength * $rangeBlocks[1];
140+
$position = $first;
141+
}
142+
143+
$closureTrackMemoryUsage = function () use (& $peakMemoryUsage) {
144+
$peakMemoryUsage = max($peakMemoryUsage, memory_get_usage());
145+
};
146+
147+
$closureFullContents = function () use (& $sizeBytes) {
148+
return str_repeat('0', $sizeBytes);
149+
};
150+
151+
$stream = $this->prophesize('Psr\Http\Message\StreamInterface');
152+
$stream->getSize()->willReturn($sizeBytes);
153+
$stream->isSeekable()->willReturn($seekable);
154+
$stream->isReadable()->willReturn(true);
155+
$stream->__toString()->will($closureFullContents);
156+
$stream->getContents()->will($closureFullContents);
157+
$stream->rewind()->willReturn(true);
158+
159+
$stream->seek(Argument::type('integer'), Argument::any())->will(function ($args) use (& $position) {
160+
$position = $args[0];
161+
return true;
162+
});
163+
164+
$stream->eof()->will(function () use (& $sizeBytes, & $position) {
165+
return ($position >= $sizeBytes);
166+
});
167+
168+
$stream->tell()->will(function () use (& $position) {
169+
return $position;
170+
});
171+
172+
$stream->read(Argument::type('integer'))->will(function ($args) use (& $position, & $peakBufferLength) {
173+
if ($args[0] > $peakBufferLength) {
174+
$peakBufferLength = $args[0];
175+
}
176+
177+
$position += $args[0];
178+
179+
return str_repeat('0', $args[0]);
180+
});
181+
182+
$response = (new Response())
183+
->withStatus(200)
184+
->withBody($stream->reveal());
185+
186+
187+
if ($rangeBlocks) {
188+
$response = $response->withHeader('Content-Range', 'bytes ' . $first . '-' . $last . '/*');
189+
}
190+
191+
ob_start(
192+
function () use (& $closureTrackMemoryUsage) {
193+
$closureTrackMemoryUsage();
194+
195+
return '';
196+
},
197+
$maxBufferLength
198+
);
199+
200+
gc_collect_cycles();
201+
202+
$this->emitter->emit($response, $maxBufferLength);
203+
204+
ob_end_flush();
205+
206+
gc_collect_cycles();
207+
208+
$localMemoryUsage = memory_get_usage();
209+
210+
$this->assertLessThanOrEqual($maxBufferLength, $peakBufferLength);
211+
$this->assertLessThanOrEqual($maxAllowedMemoryUsage, $peakMemoryUsage - $localMemoryUsage);
212+
}
213+
214+
public function emitBodyRangeProvider()
215+
{
216+
return [
217+
[true, '01234567890123456789' , ['bytes', 10, 20, '*'], 10, 1],
218+
[true, '012345678901234567890123', ['bytes', 10, 40, '*'], 10, 2],
219+
[false, '01234567890123456789' , ['bytes', 11, 20, '*'], 10, 1],
220+
[false, '012345678901234567890123', ['bytes', 11, 40, '*'], 10, 2],
221+
];
222+
}
223+
224+
/**
225+
* @dataProvider emitBodyRangeProvider
226+
*/
227+
public function testEmitBodyRange($seekable, $contents, $range, $maxBufferLength, $expectedReads = 0)
228+
{
229+
list($unit, $first, $last, $length) = $range;
230+
231+
$position = $first;
232+
233+
$stream = $this->prophesize('Psr\Http\Message\StreamInterface');
234+
$stream->getSize()->willReturn(strlen($contents));
235+
$stream->isSeekable()->willReturn($seekable);
236+
$stream->isReadable()->willReturn(true);
237+
$stream->__toString()->willReturn($contents);
238+
$stream->getContents()->willReturn($contents);
239+
$stream->rewind()->willReturn(true);
240+
241+
$stream->seek(Argument::type('integer'), Argument::any())->will(function ($args) use (& $position) {
242+
$position = $args[0];
243+
return true;
244+
});
245+
246+
$stream->eof()->will(function () use (& $contents, & $position) {
247+
return ! isset($contents[$position]);
248+
});
249+
250+
$stream->tell()->will(function () use (& $position) {
251+
return $position;
252+
});
253+
254+
$stream->read(Argument::type('integer'))->will(function ($args) use (& $contents, & $position) {
255+
$data = substr($contents, $position, $args[0]);
256+
$position += strlen($data);
257+
return $data;
258+
});
259+
260+
$response = (new Response())
261+
->withStatus(200)
262+
->withHeader('Content-Range', "$unit $first-$last/$length")
263+
->withBody($stream->reveal());
264+
265+
ob_start();
266+
$this->emitter->emit($response, $maxBufferLength);
267+
$stream->seek(Argument::type('integer'), Argument::any())->shouldBeCalledTimes($seekable ? 1 : 0);
268+
$stream->read(Argument::type('integer'))->shouldBeCalledTimes($expectedReads);
269+
$this->assertEquals(substr($contents, $first, $last - $first + 1), ob_get_clean());
270+
}
271+
61272
public function contentRangeProvider()
62273
{
63274
return [

0 commit comments

Comments
 (0)