Skip to content

[Serializer] add WebDebugToolbar collector for normalize/denormalize actions inside the serializer. #39325

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

Closed
wants to merge 19 commits into from

Conversation

Basster
Copy link
Contributor

@Basster Basster commented Dec 5, 2020

Q A
Branch? 5.x
Bug fix? no
New feature? yes
Deprecations? no
Tickets Fix #39102
License MIT
Doc PR do I need to add the collector to the docs?

Add a WebDebugToolbar collector for normalize/denormalize actions inside the serializer.

How it works?

  • All Normalizers are going to be decorated with specific implementations of AbstractTraceableNormalizer in the SerializerDebugPass.
    • All decorated normalizers will get tagged with debug.normalizer, as well.
    • I had to use three implementations here, since the Serializer utilizes a lot of instanceof checks.
  • The SerializerDataCollector collects all serializations, deserializations, normalizations & denormalizations from all services with the debug.normalizer tag.
  • On the serializer_debug.php the serializer services gets decorated with a TraceableSerializer and the SerializerDataCollector gets registered.

#SymfonyHackday

@carsonbot
Copy link

Hey!

Nice work here, I this makes me happy.

To help keep things organized, we don't allow "Draft" pull requests. Could you please click the "ready for review" button or close this PR and open a new one when you are done?

Note that a pull request does not have to be "perfect" or "ready for merge" when you first open it. We just want it to be ready for a first review.

Cheers!

Carsonbot

@Basster Basster marked this pull request as ready for review December 5, 2020 12:32
@Basster Basster requested a review from dunglas as a code owner December 5, 2020 12:32
@Basster
Copy link
Contributor Author

Basster commented Dec 5, 2020

Not to self: add a __call to the AbstractTraceableNormalizer. to delegate all calls, the decorated normalizers don't know to their delegates, if they have such kind of method.

__callStatic as well? What do you think @derrabus?

Copy link
Member

@derrabus derrabus left a comment

Choose a reason for hiding this comment

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

Looks good so far. I think we need to make sure that we limit the amount of data that we actually store in the profile. If an app serializes 120 MiB of data, we probably don't want the full output to be logged.

Copy link
Member

@jderusse jderusse left a comment

Choose a reason for hiding this comment

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

Thank you for working on this @Basster

->setArguments([$normalizerDef])
->addTag('debug.normalizer')
->setDecoratedService($id)
->setAutowired(true)
Copy link
Member

Choose a reason for hiding this comment

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

Is it needed?

same for autoconfigure

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think it's required indeed?

@jderusse jderusse added this to the 5.x milestone Dec 6, 2020
@Basster
Copy link
Contributor Author

Basster commented Dec 7, 2020

Thank you @derrabus and @jderusse for your input. I'm trying to get over your comments, as soon as I can.

@Basster
Copy link
Contributor Author

Basster commented Dec 21, 2020

__callStatic as well? What do you think @derrabus?

I think __callStatic makes no sense, because only injected normalizers will get decorated.

@Basster
Copy link
Contributor Author

Basster commented Dec 21, 2020

I have no clue, why travis fails this particular build?

@Basster Basster force-pushed the 39102-serializer-collector branch from dae8641 to f861ffe Compare March 6, 2021 12:24
@Basster
Copy link
Contributor Author

Basster commented Mar 6, 2021

I'm still convinced that's not my fault, that appveyor and travis fail, since the 5.x branch currently fails at the same places. So please @derrabus and @jderusse, it would be kind if you could look over this, again.

Copy link
Contributor

@ogizanagi ogizanagi left a comment

Choose a reason for hiding this comment

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

This is a huge work and needs to be tested in real-life applications with extensive usage of the Serializer to see how it behaves and how we can make it the most relevant.
It'll also be the the starting point for a case I left in #27262, about having Serializer metrics in the performance panel.

I think there is still a bit of work, but thank you @Basster for working on this!

} elseif ($isDenormalizer) {
$decoratorClass = TraceableDenormalizer::class;
} else {
throw new RuntimeException(sprintf('Normalizer with id "%s" neither implements NormalizerInterface nor DenormalizerInterface!', $id));
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it should rather be a LogicException, since it' a user misconfiguration/code issue.
But I also wonder why this pass would throw while the SerializerPass doesn't?

If it's interesting DX-wise, it should be in the SerializerPass (not necessarily in this PR).

->setArguments([$normalizerDef])
->addTag('debug.normalizer')
->setDecoratedService($id)
->setAutowired(true)
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think it's required indeed?

->setAutowired(true)
;

$container->setDefinition($aliasName, $decoratorDef);
Copy link
Contributor

Choose a reason for hiding this comment

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

$aliasName could be inlined here. Also what about using ->register directly?

return $this->delegate->$name(...$arguments);
}

throw new \LogicException('Unexpected method call.');
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
throw new \LogicException('Unexpected method call.');
throw new \BadMethodCallException(sprintf('Unexpected "%s" method call.', $name));

?

Comment on lines +76 to +77

public function setSerializer(SerializerInterface $serializer): void
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
public function setSerializer(SerializerInterface $serializer): void
public function setSerializer(SerializerInterface $serializer): void

use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

interface SerializerActionFactoryInterface
Copy link
Contributor

Choose a reason for hiding this comment

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

What is the use-case for this interface?
I don't think we need it

public function createDenormalization(DenormalizerInterface $denormalizer, $data, $result, string $type, string $format, array $context = []): Denormalization
{
return new Denormalization(
$denormalizer,
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to hold a reference on the (de)normalizer instance here? What about storing the classname or original service name instead?


public function createNormalization(NormalizerInterface $normalizer, $object, $result, string $format, array $context = []): Normalization
{
return new Normalization(
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as raised by @jderusse in #39325 (comment), I'm not convinced about the need of an object to store these information for this specific use-case (yet I usually agree about not using arrays for structured data).

Also related to my comment about the factory interface, this factory is almost pure internal details of the traceable normalizers and serializer, to share some methods, so are these objects. The factory could be internal and instantiated from the traceable objects, or refactored as a trait.

if we'd like to keep these objects, I think at least:

  • the factory interface should be removed
  • the factory class and these objects must be @internal
  • the factory can be instantiated from the traceable instances directly

WDYT?

@fabpot fabpot modified the milestones: 5.4, 6.1 Oct 30, 2021
Copy link
Contributor

@ogizanagi ogizanagi left a comment

Choose a reason for hiding this comment

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

In an API listing endpoint, the number of normalization calls can quickly grow and exposing each of these calls might not really be relevant nor handy for the developper.

Capture d’écran 2021-11-02 à 15 57 59

I wonder if we shouldn't target something more concise, like only showing the root method calls to the serializer's serialize, deserialize, normalize and denormalize methods, instead of decorating each normalizers/denormalizers.

I know this would defeat some use-cases of this profiler panel, but debugging each (de)normalizers output might not be the role of this panel.

We could also add the context in which was made this call (source file and line N°), as in the validation panel:

Capture d’écran 2021-11-02 à 16 34 32

The table output might not be the best to read neither:

Capture d’écran 2021-11-02 à 16 01 00

Perhaps reversing the row/columns with a unique table per call would be better (as done for the Messenger panel).

<td class="nowrap">{{ normalization.format }}</td>
<td class="nowrap">{{ normalization.normalizer | abbr_class }}</td>
<td>{{ profiler_dump(normalization.data, maxDepth=2) }}</td>
<td>{{ profiler_dump(normalization.result, maxDepth=2) }}</td>
Copy link
Contributor

Choose a reason for hiding this comment

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

In case the normalization result is a string, it'll fail with:

Argument 2 passed to Symfony\Bundle\WebProfilerBundle\Twig\WebProfilerExtension::dumpData() must be an instance of Symfony\Component\VarDumper\Cloner\Data, string given

The trick is to use seek for such cases:

Suggested change
<td>{{ profiler_dump(normalization.result, maxDepth=2) }}</td>
<td>{{ profiler_dump(normalization.seek('result'), maxDepth=2) }}</td>

<td class="font-normal text-small text-muted nowrap">{{ loop.index }}</td>
<td class="nowrap">{{ normalization.format }}</td>
<td class="nowrap">{{ normalization.normalizer | abbr_class }}</td>
<td>{{ profiler_dump(normalization.data, maxDepth=2) }}</td>
Copy link
Contributor

Choose a reason for hiding this comment

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

Since the normalizer input is usually an object, but the interface accepts mixed, it could be a string as well, so same here, use seek

<td class="nowrap">{{ denormalization.denormalizer | abbr_class }}</td>
<td class="nowrap">{{ denormalization.type | abbr_class }}</td>
<td>{{ profiler_dump(denormalization.data, maxDepth=2) }}</td>
<td>{{ profiler_dump(denormalization.result, maxDepth=2) }}</td>
Copy link
Contributor

Choose a reason for hiding this comment

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

Same for denormalization data and result, use seek

<td class="font-normal text-small text-muted nowrap">{{ loop.index }}</td>
<td class="nowrap">{{ serialization.format }}</td>
<td>{{ profiler_dump(serialization.data, maxDepth=2) }}</td>
<td>{{ serialization.result }}</td>
Copy link
Contributor

Choose a reason for hiding this comment

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

Serialization output is always a string, so not using profiler_dump is okay, but using it can streamline the rendering in the profiler, and ellipse too long outputs:

Suggested change
<td>{{ serialization.result }}</td>
<td>{{ profiler_dump(serialization.seek('result')) }}</td>

Capture d’écran 2021-11-02 à 15 38 58

(should probably add a word-break: break-word; at least)

vs

Capture d’écran 2021-11-02 à 15 38 18

don't know if we want it or not.

<td class="font-normal text-small text-muted nowrap">{{ loop.index }}</td>
<td class="nowrap">{{ deserialization.format }}</td>
<td class="nowrap">{{ deserialization.type | abbr_class }}</td>
<td>{{ deserialization.data }}</td>
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here

<td class="nowrap">{{ deserialization.format }}</td>
<td class="nowrap">{{ deserialization.type | abbr_class }}</td>
<td>{{ deserialization.data }}</td>
<td>{{ profiler_dump(deserialization.result, maxDepth=2) }}</td>
Copy link
Contributor

Choose a reason for hiding this comment

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

Deserialization result might be a string in some cases, so let's use seek here too


<footer>
<small class="text-muted">
The icon is taken from <a href="https://fontawesome.com/icons/exchange-alt?style=solid">fontawesome</a>
Copy link
Contributor

Choose a reason for hiding this comment

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

We'll probably add our own icon, once this PR is ready

return static function (ContainerConfigurator $container) {
$container->services()
->set('debug.serializer', TraceableSerializer::class)
->decorate('serializer', null, 255)
Copy link
Contributor

Choose a reason for hiding this comment

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

The Serializer class also implements normalizers and encoders interfaces, hence the places in userland were the @serializer service was injected and using encode, decode, normalize or denormalize will fail with:

Attempted to call an undefined method named "decode" of class "Symfony\Component\Serializer\Debug\TraceableSerializer".

and the DI aliases on the respective interfaces won't work anymore.

So decorating this service directly might not be a good idea?

We could decorate each alias instead, but then no data would be collected where the @serializer service itself is injected.
So, should we have a traceable serializer implementing all of these interfaces? 🤔

{% endblock %}

{% block menu %}
{% set disable = collector.serializations | length == 0 and collector.deserializations | length == 0 %}
Copy link
Contributor

Choose a reason for hiding this comment

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

Even if there is no serialization/deserialization, there could be some normalize and denormalize calls that would be caught by this panel and should make it active.

</div>
<div class="metric">
<span class="value">{{ collector.denormalizations | length }}</span>
<span class="label">Denormalizations</span>
Copy link
Contributor

Choose a reason for hiding this comment

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

We already have these numbers in the tabs below, no need to duplicate here I guess

@chalasr
Copy link
Member

chalasr commented Mar 22, 2022

Closing in favor of #45656 as it's up to date and it addresses the main issues reported here. Thank you for the PR.

@chalasr chalasr closed this Mar 22, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[RFC] WebDebugToolbar collector for normalize/denormalize actions inside the serializer
7 participants