Skip to content

Commit f40c40b

Browse files
authored
Merge pull request #7395 from magento-performance/MCP-789
MCP-789
2 parents 01c23b1 + e0a0f4a commit f40c40b

File tree

3 files changed

+204
-63
lines changed

3 files changed

+204
-63
lines changed

app/code/Magento/Checkout/view/frontend/web/js/model/totals.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ define([
2222
quoteItems(newValue.items);
2323
});
2424

25-
if (!isNaN(subtotalAmount) && quoteSubtotal !== subtotalAmount) {
25+
if (!isNaN(subtotalAmount) && quoteSubtotal !== subtotalAmount && quoteSubtotal !== 0) {
2626
customerData.reload(['cart'], false);
2727
}
2828

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
namespace Magento\Framework\GraphQl\GraphQlSchemaStitching;
10+
11+
use Magento\Framework\GraphQlSchemaStitching\GraphQlReader;
12+
use Magento\Framework\ObjectManagerInterface;
13+
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
14+
use PHPUnit\Framework\MockObject\MockObject;
15+
use PHPUnit\Framework\TestCase;
16+
use Magento\Framework\Config\FileResolverInterface;
17+
use Magento\Framework\Config\FileIterator;
18+
use Magento\Framework\Component\ComponentRegistrar;
19+
20+
/**
21+
* Test of the stitching of graphql schemas together
22+
*/
23+
class GraphQlReaderTest extends TestCase
24+
{
25+
/**
26+
* Object Manager Instance
27+
*
28+
* @var ObjectManager
29+
*/
30+
private $objectManager;
31+
32+
/**
33+
* @var GraphQlReader|MockObject
34+
*/
35+
private $graphQlReader;
36+
37+
protected function setUp(): void
38+
{
39+
/** @var ObjectManagerInterface $objectManager */
40+
$this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager();
41+
42+
$this->graphQlReader = $this->objectManager->create(
43+
GraphQlReader::class
44+
);
45+
}
46+
47+
/**
48+
* This test ensures that the global graphql schemas have all the required dependencies and can be stitched together
49+
*
50+
* The $results variables contains the actual schema as it will be on a production site which will vary per each
51+
* update of magento, so asserting the array matches the entire schema does not make full sense here as any change
52+
* in graphql in any magento module would break the test.
53+
*
54+
* Testing this way means we do not need to store the module meta data that was introduced in
55+
* https://github.com/magento/magento2/pull/28747 which means we can greatly improve the performance of this
56+
*/
57+
public function testStitchGlobalGraphQLSchema()
58+
{
59+
$results = $this->graphQlReader->read('global');
60+
61+
$this->assertArrayHasKey('Price', $results);
62+
$this->assertArrayHasKey('Query', $results);
63+
$this->assertArrayHasKey('Mutation', $results);
64+
$this->assertArrayHasKey('ProductInterface', $results);
65+
$this->assertArrayHasKey('SimpleProduct', $results);
66+
}
67+
}

lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader.php

Lines changed: 136 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
use GraphQL\Type\Definition\ScalarType;
1111
use GraphQL\Utils\BuildSchema;
12-
use Magento\Framework\Component\ComponentRegistrar;
1312
use Magento\Framework\Config\FileResolverInterface;
1413
use Magento\Framework\Config\ReaderInterface;
1514
use Magento\Framework\GraphQl\Type\TypeManagement;
@@ -48,11 +47,6 @@ class GraphQlReader implements ReaderInterface
4847
*/
4948
private $defaultScope;
5049

51-
/**
52-
* @var ComponentRegistrar
53-
*/
54-
private static $componentRegistrar;
55-
5650
/**
5751
* @param FileResolverInterface $fileResolver
5852
* @param TypeReaderComposite $typeReader
@@ -75,7 +69,7 @@ public function __construct(
7569

7670
/**
7771
* @inheritdoc
78-
*
72+
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
7973
* @param string|null $scope
8074
* @return array
8175
*/
@@ -84,28 +78,103 @@ public function read($scope = null): array
8478
$results = [];
8579
$scope = $scope ?: $this->defaultScope;
8680
$schemaFiles = $this->fileResolver->get($this->fileName, $scope);
87-
8881
if (!count($schemaFiles)) {
8982
return $results;
9083
}
9184

9285
/**
86+
* Gather as many schema together to be parsed in one go for performance
87+
* Collect any duplicate types in an array to retry after the initial large parse
88+
*
9389
* Compatible with @see GraphQlReader::parseTypes
9490
*/
91+
$typesToRedo = [];
9592
$knownTypes = [];
96-
foreach ($schemaFiles as $filePath => $partialSchemaContent) {
97-
$partialSchemaTypes = $this->parseTypes($partialSchemaContent);
93+
foreach ($schemaFiles as $partialSchemaContent) {
94+
$partialSchemaTypes = $this->parseTypesWithUnionHandling($partialSchemaContent);
95+
96+
// Filter out duplicated ones and save them into a list to be retried
97+
$tmpTypes = $knownTypes;
98+
foreach ($partialSchemaTypes as $intendedKey => $partialSchemaType) {
99+
if (isset($tmpTypes[$intendedKey])) {
100+
if (!isset($typesToRedo[$intendedKey])) {
101+
$typesToRedo[$intendedKey] = [];
102+
}
103+
$typesToRedo[$intendedKey][] = $partialSchemaType;
104+
continue;
105+
}
106+
$tmpTypes[$intendedKey] = $partialSchemaType;
107+
}
108+
$knownTypes = $tmpTypes;
109+
}
110+
111+
/**
112+
* Read this large batch of data, this builds most of the $results array
113+
*/
114+
$schemaContent = implode("\n", $knownTypes);
115+
$results = $this->readPartialTypes($schemaContent);
116+
117+
/**
118+
* Go over the list of types to be retried and batch them up into as few batches as possible
119+
*/
120+
$typesToRedoBatches = [];
121+
foreach ($typesToRedo as $type => $batches) {
122+
foreach ($batches as $id => $data) {
123+
if (!isset($typesToRedoBatches[$id])) {
124+
$typesToRedoBatches[$id] = [];
125+
}
126+
$typesToRedoBatches[$id][$type] = $data;
127+
}
128+
}
98129

99-
// Keep declarations from current partial schema, add missing declarations from all previously read schemas
100-
$knownTypes = $partialSchemaTypes + $knownTypes;
101-
$schemaContent = implode("\n", $knownTypes);
130+
/**
131+
* Process each remaining batch with the minimal amount of additional schema data for performance
132+
*/
133+
foreach ($typesToRedoBatches as $typesToRedoBatch) {
134+
$typesToUse = $this->getTypesToUse($typesToRedoBatch, $knownTypes);
135+
$knownTypes = $typesToUse + $knownTypes;
136+
$schemaContent = implode("\n", $typesToUse);
102137

103138
$partialResults = $this->readPartialTypes($schemaContent);
104139
$results = array_replace_recursive($results, $partialResults);
105-
$results = $this->addModuleNameToTypes($results, $filePath);
106140
}
107141

108-
return $this->copyInterfaceFieldsToConcreteTypes($results);
142+
$results = $this->copyInterfaceFieldsToConcreteTypes($results);
143+
return $results;
144+
}
145+
146+
/**
147+
* Get the minimum amount of additional types so that performance is improved
148+
*
149+
* The use of a strpos check here is a bit odd in the context of feeding data into an AST but for the performance
150+
* gains and to prevent downtime it is necessary
151+
*
152+
* @link https://github.com/webonyx/graphql-php/issues/244
153+
* @link https://github.com/webonyx/graphql-php/issues/244#issuecomment-383912418
154+
*
155+
* @param array $typesToRedoBatch
156+
* @param array $types
157+
* @return array
158+
*/
159+
private function getTypesToUse($typesToRedoBatch, $types): array
160+
{
161+
$totalKnownSymbolsCount = count($typesToRedoBatch) + count($types);
162+
163+
$typesToUse = $typesToRedoBatch;
164+
for ($i=0; $i < $totalKnownSymbolsCount; $i++) {
165+
$changesMade = false;
166+
$schemaContent = implode("\n", $typesToUse);
167+
foreach ($types as $type => $schema) {
168+
if ((!isset($typesToUse[$type]) && strpos($schemaContent, $type) !== false)) {
169+
$typesToUse[$type] = $schema;
170+
$changesMade = true;
171+
}
172+
}
173+
if (!$changesMade) {
174+
break;
175+
}
176+
}
177+
return $typesToUse;
109178
}
110179

111180
/**
@@ -137,6 +206,56 @@ private function readPartialTypes(string $graphQlSchemaContent): array
137206
return $this->removePlaceholderFromResults($partialResults);
138207
}
139208

209+
/**
210+
* Extract types as string from a larger string that represents the graphql schema using regular expressions
211+
*
212+
* The regex in parseTypes does not have the ability to split out the union data from the type below it for example
213+
*
214+
* > union X = Y | Z
215+
* >
216+
* > type foo {}
217+
*
218+
* This would produce only type key from parseTypes, X, which would contain also the type foo entry.
219+
*
220+
* This wrapper does some post processing as a workaround to split out the union data from the type data below it
221+
* which would give us two entries, X and foo
222+
*
223+
* @param string $graphQlSchemaContent
224+
* @return string[] [$typeName => $typeDeclaration, ...]
225+
*/
226+
private function parseTypesWithUnionHandling(string $graphQlSchemaContent): array
227+
{
228+
$types = $this->parseTypes($graphQlSchemaContent);
229+
230+
/*
231+
* A union schema contains also the data from the schema below it
232+
*
233+
* If there are two newlines in this union schema then it has data below its definition, meaning it contains
234+
* type information not relevant to its actual type
235+
*/
236+
$unionTypes = array_filter(
237+
$types,
238+
function ($t) {
239+
return (strpos($t, 'union ') !== false) && (strpos($t, PHP_EOL . PHP_EOL) !== false);
240+
}
241+
);
242+
243+
foreach ($unionTypes as $type => $schema) {
244+
$splitSchema = explode(PHP_EOL . PHP_EOL, $schema);
245+
// Get the type data at the bottom, this will be the additional type data not related to the union
246+
$additionalTypeSchema = end($splitSchema);
247+
// Parse the additional type from the bottom so we can have its type key => schema pair
248+
$additionalTypeData = $this->parseTypes($additionalTypeSchema);
249+
// Fix the union type schema so it does not contain the definition below it
250+
$types[$type] = str_replace($additionalTypeSchema, '', $schema);
251+
// Append the additional data to types array
252+
$additionalTypeKey = array_key_first($additionalTypeData);
253+
$types[$additionalTypeKey] = $additionalTypeData[$additionalTypeKey];
254+
}
255+
256+
return $types;
257+
}
258+
140259
/**
141260
* Extract types as string from a larger string that represents the graphql schema using regular expressions
142261
*
@@ -147,7 +266,7 @@ private function parseTypes(string $graphQlSchemaContent): array
147266
{
148267
$typeKindsPattern = '(type|interface|union|enum|input)';
149268
$typeNamePattern = '([_A-Za-z][_0-9A-Za-z]+)';
150-
$typeDefinitionPattern = '([^\{]*)(\{[^\}]*\})';
269+
$typeDefinitionPattern = '([^\{\}]*)(\{[^\}]*\})';
151270
$spacePattern = '[\s\t\n\r]+';
152271

153272
preg_match_all(
@@ -259,7 +378,7 @@ private function addPlaceHolderInSchema(string $graphQlSchemaContent): string
259378
$typesKindsPattern = '(type|interface|input|union)';
260379
$enumKindsPattern = '(enum)';
261380
$typeNamePattern = '([_A-Za-z][_0-9A-Za-z]+)';
262-
$typeDefinitionPattern = '([^\{]*)(\{[\s\t\n\r^\}]*\})';
381+
$typeDefinitionPattern = '([^\{\}]*)(\{[\s\t\n\r^\}]*\})';
263382
$spacePattern = '([\s\t\n\r]+)';
264383

265384
//add placeholder in empty types
@@ -299,49 +418,4 @@ private function removePlaceholderFromResults(array $partialResults): array
299418
}
300419
return $partialResults;
301420
}
302-
303-
/**
304-
* Get a module name by file path
305-
*
306-
* @param string $file
307-
* @return string
308-
*/
309-
private static function getModuleNameForRelevantFile(string $file): string
310-
{
311-
if (!isset(self::$componentRegistrar)) {
312-
self::$componentRegistrar = new ComponentRegistrar();
313-
}
314-
$foundModuleName = '';
315-
foreach (self::$componentRegistrar->getPaths(ComponentRegistrar::MODULE) as $moduleName => $moduleDir) {
316-
if (strpos($file, $moduleDir . '/') !== false) {
317-
$foundModuleName = str_replace('_', '\\', $moduleName);
318-
break;
319-
}
320-
}
321-
322-
return $foundModuleName;
323-
}
324-
325-
/**
326-
* Add a module name to types
327-
*
328-
* @param array $source
329-
* @param string $filePath
330-
* @return array
331-
*/
332-
private function addModuleNameToTypes(array $source, string $filePath): array
333-
{
334-
foreach ($source as $typeName => $typeDefinition) {
335-
if (!isset($typeDefinition['module'])) {
336-
$hasTypeResolver = (bool)($typeDefinition['typeResolver'] ?? false);
337-
$hasImplements = (bool)($typeDefinition['implements'] ?? false);
338-
$typeDefinition = (bool)($typeDefinition['type'] ?? false);
339-
if ((($typeDefinition === InterfaceType::GRAPHQL_INTERFACE && $hasTypeResolver) || $hasImplements)) {
340-
$source[$typeName]['module'] = self::getModuleNameForRelevantFile($filePath);
341-
}
342-
}
343-
}
344-
345-
return $source;
346-
}
347421
}

0 commit comments

Comments
 (0)