Skip to content

Commit 9e3519e

Browse files
authored
Merge pull request #51 from veewee/feature/phpbench-setup
Add phpbench benchmarks with CI regression detection
2 parents a38af3e + b4e60b9 commit 9e3519e

11 files changed

Lines changed: 1331 additions & 2 deletions

.github/workflows/benchmarks.yaml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: Benchmarks
2+
3+
on: [pull_request]
4+
jobs:
5+
run:
6+
runs-on: ubuntu-latest
7+
name: Performance regression check
8+
steps:
9+
- name: Checkout base branch
10+
uses: actions/checkout@master
11+
with:
12+
ref: ${{ github.base_ref }}
13+
- name: Install PHP
14+
uses: shivammathur/setup-php@master
15+
with:
16+
php-version: '8.5'
17+
tools: 'composer:v2'
18+
extensions: mbstring, dom, soap
19+
coverage: none
20+
- name: Install dependencies (base)
21+
run: composer update --prefer-dist --no-progress --no-suggest
22+
- name: Benchmark baseline (base branch)
23+
run: |
24+
vendor/bin/phpbench run benchmarks/ \
25+
--tag=baseline \
26+
--store \
27+
--report=aggregate
28+
- name: Checkout PR branch
29+
uses: actions/checkout@master
30+
with:
31+
clean: false
32+
- name: Install dependencies (PR)
33+
run: composer update --prefer-dist --no-progress --no-suggest
34+
- name: Benchmark PR branch vs baseline
35+
run: |
36+
vendor/bin/phpbench run benchmarks/ \
37+
--ref=baseline \
38+
--report=aggregate

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ vendor
22
composer.lock
33
.phpunit.cache
44
.php-cs-fixer.cache
5+
.phpbench

benchmarks/BenchSoapClient.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Soap\Encoding\Benchmarks;
6+
7+
/**
8+
* SoapClient override that captures requests and returns mock responses.
9+
* Used by benchmarks to measure ext-soap encode/decode without network I/O.
10+
*
11+
* @internal
12+
*/
13+
class BenchSoapClient extends \SoapClient
14+
{
15+
public ?string $mockResponse = null;
16+
17+
public function __doRequest(string $request, string $location, string $action, int $version, bool $oneWay = false): ?string
18+
{
19+
return $this->mockResponse;
20+
}
21+
}
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Soap\Encoding\Benchmarks;
6+
7+
use Soap\Encoding\Driver;
8+
use Soap\Encoding\EncoderRegistry;
9+
use Soap\Engine\HttpBinding\SoapResponse;
10+
use Soap\Wsdl\Loader\CallbackLoader;
11+
use Soap\WsdlReader\Metadata\Wsdl1MetadataProvider;
12+
use Soap\WsdlReader\Wsdl1Reader;
13+
14+
/**
15+
* Shared setup for encode/decode benchmarks.
16+
* Complex type: itemType (9 properties including nested addressType).
17+
*/
18+
trait ComplexTypeBenchTrait
19+
{
20+
protected Driver $literalDriver;
21+
protected Driver $encodedDriver;
22+
protected object $singleItem;
23+
/** @var list<object> */
24+
protected array $items;
25+
protected SoapResponse $literalResponse;
26+
protected SoapResponse $encodedResponse;
27+
28+
protected BenchSoapClient $extSoapLiteral;
29+
protected BenchSoapClient $extSoapEncoded;
30+
protected string $minimalResponse;
31+
protected string $fullResponse;
32+
33+
public function setUp(): void
34+
{
35+
$this->singleItem = (object) [
36+
'id' => 42,
37+
'name' => 'Widget Pro',
38+
'description' => 'A high-quality widget for professional use',
39+
'price' => 29.99,
40+
'quantity' => 100,
41+
'active' => true,
42+
'sku' => 'WDG-PRO-001',
43+
'category' => 'Electronics',
44+
'address' => (object) [
45+
'street' => '123 Main St',
46+
'city' => 'Springfield',
47+
'zip' => '62701',
48+
],
49+
];
50+
51+
$this->items = [];
52+
for ($i = 0; $i < 500; $i++) {
53+
$this->items[] = (object) [
54+
'id' => $i,
55+
'name' => 'Widget ' . $i,
56+
'description' => 'Description for widget ' . $i,
57+
'price' => 9.99 + $i,
58+
'quantity' => $i * 10,
59+
'active' => $i % 2 === 0,
60+
'sku' => 'WDG-' . str_pad((string) $i, 5, '0', STR_PAD_LEFT),
61+
'category' => 'Category ' . ($i % 10),
62+
'address' => (object) [
63+
'street' => $i . ' Elm St',
64+
'city' => 'City ' . ($i % 50),
65+
'zip' => str_pad((string) ($i % 99999), 5, '0', STR_PAD_LEFT),
66+
],
67+
];
68+
}
69+
70+
// php-soap/encoding
71+
$this->literalDriver = $this->buildPhpSoapDriver('literal');
72+
$this->encodedDriver = $this->buildPhpSoapDriver('encoded');
73+
74+
$this->literalResponse = new SoapResponse(
75+
$this->literalDriver->encode('test', [$this->singleItem])->getRequest()
76+
);
77+
$this->encodedResponse = new SoapResponse(
78+
$this->encodedDriver->encode('test', [$this->singleItem])->getRequest()
79+
);
80+
81+
// ext-soap
82+
$this->extSoapLiteral = $this->buildExtSoapClient('literal');
83+
$this->extSoapEncoded = $this->buildExtSoapClient('encoded');
84+
85+
$this->minimalResponse = '<?xml version="1.0" encoding="UTF-8"?>'
86+
. '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">'
87+
. '<SOAP-ENV:Body><ns1:testResponse xmlns:ns1="http://test-uri/"/>'
88+
. '</SOAP-ENV:Body></SOAP-ENV:Envelope>';
89+
90+
$this->fullResponse = '<?xml version="1.0" encoding="UTF-8"?>'
91+
. '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">'
92+
. '<SOAP-ENV:Body><ns1:testResponse xmlns:ns1="http://test-uri/">'
93+
. '<testParam><id>42</id><name>Widget Pro</name>'
94+
. '<description>A high-quality widget for professional use</description>'
95+
. '<price>29.99</price><quantity>100</quantity><active>true</active>'
96+
. '<sku>WDG-PRO-001</sku><category>Electronics</category>'
97+
. '<address><street>123 Main St</street><city>Springfield</city><zip>62701</zip></address>'
98+
. '</testParam></ns1:testResponse>'
99+
. '</SOAP-ENV:Body></SOAP-ENV:Envelope>';
100+
101+
// Warmup ext-soap
102+
$this->extSoapLiteral->mockResponse = $this->minimalResponse;
103+
$this->extSoapLiteral->__soapCall('test', ['testParam' => $this->singleItem]);
104+
$this->extSoapEncoded->mockResponse = $this->minimalResponse;
105+
$this->extSoapEncoded->__soapCall('test', ['testParam' => $this->singleItem]);
106+
}
107+
108+
private function buildPhpSoapDriver(string $use): Driver
109+
{
110+
$encodingStyle = $use === 'encoded'
111+
? ' encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"'
112+
: '';
113+
114+
$wsdl = <<<WSDL
115+
<definitions name="BenchTest"
116+
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
117+
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
118+
xmlns:tns="http://test-uri/"
119+
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
120+
xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
121+
xmlns="http://schemas.xmlsoap.org/wsdl/"
122+
targetNamespace="http://test-uri/">
123+
<types>
124+
<schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="http://test-uri/">
125+
<xsd:import namespace="http://schemas.xmlsoap.org/soap/encoding/" />
126+
<xsd:import namespace="http://schemas.xmlsoap.org/wsdl/" />
127+
<complexType name="addressType">
128+
<sequence>
129+
<element name="street" type="xsd:string"/>
130+
<element name="city" type="xsd:string"/>
131+
<element name="zip" type="xsd:string"/>
132+
</sequence>
133+
</complexType>
134+
<complexType name="itemType">
135+
<sequence>
136+
<element name="id" type="xsd:int"/>
137+
<element name="name" type="xsd:string"/>
138+
<element name="description" type="xsd:string"/>
139+
<element name="price" type="xsd:float"/>
140+
<element name="quantity" type="xsd:int"/>
141+
<element name="active" type="xsd:boolean"/>
142+
<element name="sku" type="xsd:string"/>
143+
<element name="category" type="xsd:string"/>
144+
<element name="address" type="tns:addressType"/>
145+
</sequence>
146+
</complexType>
147+
</schema>
148+
</types>
149+
<message name="testMessage">
150+
<part name="testParam" type="tns:itemType"/>
151+
</message>
152+
<portType name="testPortType">
153+
<operation name="test">
154+
<input message="testMessage"/>
155+
<output message="testMessage"/>
156+
</operation>
157+
</portType>
158+
<binding name="testBinding" type="testPortType">
159+
<soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
160+
<operation name="test">
161+
<soap:operation soapAction="#test" style="rpc"/>
162+
<input><soap:body use="{$use}" namespace="http://test-uri/"{$encodingStyle}/></input>
163+
<output><soap:body use="{$use}" namespace="http://test-uri/"{$encodingStyle}/></output>
164+
</operation>
165+
</binding>
166+
<service name="testService">
167+
<port name="testPort" binding="tns:testBinding">
168+
<soap:address location="test://"/>
169+
</port>
170+
</service>
171+
</definitions>
172+
WSDL;
173+
174+
$wsdlObject = (new Wsdl1Reader(
175+
new CallbackLoader(static fn (): string => $wsdl)
176+
))('file.wsdl');
177+
178+
$registry = EncoderRegistry::default();
179+
$metadata = (new Wsdl1MetadataProvider($wsdlObject))->getMetadata();
180+
181+
return Driver::createFromMetadata($metadata, $wsdlObject->namespaces, $registry);
182+
}
183+
184+
private function buildExtSoapClient(string $use): BenchSoapClient
185+
{
186+
$encodingStyle = $use === 'encoded'
187+
? ' encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"'
188+
: '';
189+
190+
$wsdl = <<<WSDL
191+
<?xml version="1.0" encoding="UTF-8"?>
192+
<definitions name="BenchTest"
193+
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
194+
xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
195+
xmlns:tns="http://test-uri/"
196+
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
197+
xmlns="http://schemas.xmlsoap.org/wsdl/"
198+
targetNamespace="http://test-uri/">
199+
<types>
200+
<xsd:schema targetNamespace="http://test-uri/">
201+
<xsd:import namespace="http://schemas.xmlsoap.org/soap/encoding/" />
202+
<xsd:complexType name="addressType">
203+
<xsd:sequence>
204+
<xsd:element name="street" type="xsd:string"/>
205+
<xsd:element name="city" type="xsd:string"/>
206+
<xsd:element name="zip" type="xsd:string"/>
207+
</xsd:sequence>
208+
</xsd:complexType>
209+
<xsd:complexType name="itemType">
210+
<xsd:sequence>
211+
<xsd:element name="id" type="xsd:int"/>
212+
<xsd:element name="name" type="xsd:string"/>
213+
<xsd:element name="description" type="xsd:string"/>
214+
<xsd:element name="price" type="xsd:float"/>
215+
<xsd:element name="quantity" type="xsd:int"/>
216+
<xsd:element name="active" type="xsd:boolean"/>
217+
<xsd:element name="sku" type="xsd:string"/>
218+
<xsd:element name="category" type="xsd:string"/>
219+
<xsd:element name="address" type="tns:addressType"/>
220+
</xsd:sequence>
221+
</xsd:complexType>
222+
</xsd:schema>
223+
</types>
224+
<message name="testMessage"><part name="testParam" type="tns:itemType"/></message>
225+
<portType name="testPortType">
226+
<operation name="test"><input message="tns:testMessage"/><output message="tns:testMessage"/></operation>
227+
</portType>
228+
<binding name="testBinding" type="tns:testPortType">
229+
<soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
230+
<operation name="test">
231+
<soap:operation soapAction="#test" style="rpc"/>
232+
<input><soap:body use="{$use}" namespace="http://test-uri/"{$encodingStyle}/></input>
233+
<output><soap:body use="{$use}" namespace="http://test-uri/"{$encodingStyle}/></output>
234+
</operation>
235+
</binding>
236+
<service name="testService">
237+
<port name="testPort" binding="tns:testBinding"><soap:address location="test://"/></port>
238+
</service>
239+
</definitions>
240+
WSDL;
241+
242+
$wsdlFile = tempnam(sys_get_temp_dir(), 'wsdl_') . '.wsdl';
243+
file_put_contents($wsdlFile, $wsdl);
244+
245+
$client = new BenchSoapClient($wsdlFile, [
246+
'trace' => true,
247+
'cache_wsdl' => WSDL_CACHE_NONE,
248+
]);
249+
250+
unlink($wsdlFile);
251+
252+
return $client;
253+
}
254+
}

0 commit comments

Comments
 (0)