Skip to content
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

Introduce ILO_URL_Metric to encapsulate core data structure #988

Merged
merged 23 commits into from Feb 29, 2024

Conversation

westonruter
Copy link
Member

@westonruter westonruter commented Feb 15, 2024

Summary

This implements these aspects of #933:

Introduce class to represent a single URL metric (e.g. ILO_URL_Metric).

  • This would mostly be a value object with properties like url, slug, viewport, elements.
  • The class could potentially have private logic to take care of validation.
  • The elements entry may deserve some further thought, particularly around how it could work considering additional use-cases than LCP elements in the future.
  • Functions that today return an array of URL metric associative arrays should going forward return an array of URL metric class instances (e.g. ilo_parse_stored_url_metrics()).

Subsequent points will be addressed in follow-up PRs.

Relevant technical choices

  • A new class called ILO_URL_Metric is used instead of passing around an array of the same data.
  • The ILO_URL_Metric class contains the validation logic to ensure that the data is as expected.
  • The REST API route re-uses the JSON schema defined in the ILO_URL_Metric class to validate requests.
  • The ILO_URL_Metric class now requires that a timestamp be provided at construction time, which removes this concern from ilo_unshift_url_metrics().
  • Validation logic is applied to the URL metrics pulled from the ilo_url_metrics post type. This ensures that if the data format changes, any prior entries will just be omitted.
  • Static analysis is further improved with typing and addressing various inspection issues flagged by PhpStorm.

Checklist

  • PR has either [Focus] or Infrastructure label.
  • PR has a [Type] label.
  • PR has a milestone or the no milestone label.

@westonruter westonruter added [Type] Feature A new feature within an existing module [Focus] Images Issues related to the Images focus area no milestone PRs that do not have a defined milestone for release [Plugin] Optimization Detective Issues for the Optimization Detective plugin labels Feb 15, 2024
Comment on lines -141 to -143
$this->assertArrayHasKey( 'timestamp', $url_metrics[0] );
$this->assertIsFloat( $url_metrics[0]['timestamp'] );
$this->assertLessThanOrEqual( microtime( true ), $url_metrics[0]['timestamp'] );
Copy link
Member Author

Choose a reason for hiding this comment

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

Irrelevant to test now that the timestamp is required at the time of instantiation of ILO_URL_Metric. Previously it would get added by ilo_unshift_url_metrics().

Same goes with the deleted assertions below.

Comment on lines -144 to -146
if ( ! isset( $a['timestamp'] ) || ! isset( $b['timestamp'] ) ) {
return 0;
}
Copy link
Member Author

Choose a reason for hiding this comment

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

No need for this now that the timestamp is guaranteed to be set.

*/
function ilo_unshift_url_metrics( array $url_metrics, array $validated_url_metric, array $breakpoints, int $sample_size ): array {
$validated_url_metric['timestamp'] = microtime( true );
Copy link
Member Author

Choose a reason for hiding this comment

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

This is now set more naturally in ilo_handle_rest_request()

Comment on lines -257 to -260
if ( ! isset( $url_metric['viewport']['width'] ) ) {
continue;
}

Copy link
Member Author

Choose a reason for hiding this comment

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

No need for this anymore since the property is guaranteed to exist.

}

return array_values(
array_filter(
$url_metrics,
static function ( $url_metric ) use ( $trigger_error ) {
// TODO: If we wanted, we could use the JSON Schema to validate the stored metrics.
Copy link
Member Author

Choose a reason for hiding this comment

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

This TODO is now done

return true;
},
),
'viewport' => array(
Copy link
Member Author

Choose a reason for hiding this comment

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

Schema definition is moved to ILO_URL_Metrics.

@westonruter westonruter marked this pull request as ready for review February 22, 2024 06:42
Copy link

github-actions bot commented Feb 22, 2024

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: westonruter <westonruter@git.wordpress.org>
Co-authored-by: thelovekesh <thelovekesh@git.wordpress.org>
Co-authored-by: mukeshpanchal27 <mukesh27@git.wordpress.org>
Co-authored-by: adamsilverstein <adamsilverstein@git.wordpress.org>
Co-authored-by: felixarntz <flixos90@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@westonruter westonruter changed the title Use classes to encapsulate data structures used in Image Loading Optimization Introduce ILO_URL_Metric to encapsulate core data structure Feb 22, 2024
}

try {
// TODO: This is re-validating the data which has been stored in the post type. This ensures it remains valid, but is it overkill?
Copy link
Member

Choose a reason for hiding this comment

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

feels fine to me, defensive coding

Copy link
Member Author

Choose a reason for hiding this comment

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

My only hesitation is that this additional validation adds latency, so it negatively impacts TTFB. However, I haven't profiled the JSON Schema validation logic, so this may not be a big deal.

Copy link
Member

@adamsilverstein adamsilverstein 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, left one comment

Copy link
Member

@felixarntz felixarntz left a comment

Choose a reason for hiding this comment

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

@westonruter This mostly looks great. Just a few small points of feedback.

Co-authored-by: Felix Arntz <felixarntz@users.noreply.github.com>
Copy link
Member

@felixarntz felixarntz left a comment

Choose a reason for hiding this comment

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

@westonruter A few additional thoughts for consideration.

Comment on lines +15 to +33
final class ILO_URL_Metric implements JsonSerializable {

/**
* Data.
*
* @var array{
* timestamp: int,
* viewport: array{ width: int, height: int },
* elements: array<array{
* isLCP: bool,
* isLCPCandidate: bool,
* xpath: string,
* intersectionRatio: float,
* intersectionRect: array{ width: int, height: int },
* boundingClientRect: array{ width: int, height: int },
* }>
* }
*/
private $data;
Copy link
Member

Choose a reason for hiding this comment

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

Wouldn't it be valuable to store the $url and $slug in this class as well? Maybe they could be separate properties from $data, but I find it a bit odd to have a class for a URL metric exclude those identifying variables.

I think it could simplify some things by putting the responsibility for this information purely into the class representing the entity. We could still use associative arrays keyed by URL or slug if we need it, but I think it would be cleaner to have it covered by this class in its entirety - unless I'm missing something.

Alternatively, if there are scenarios where URL and slug should be decoupled from the actual metric data, maybe we could separately introduce a class that is a value object representing URL and slug, and it could also provide the methods to create and validate the nonces etc. Just an idea.

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, the thing is that there are multiple URL metrics stored for a given response. The post type stores this list of URL metrics, based on the number of breakpoints and the sample size. So if we store the URL and slug with each ILO_URL_Metric we're storing multiple copies of the same data. The URL/slug is more higher level metadata which is only needed for the post type. We don't need the URL or slug except to get/set the post. I'd prefer to not include the URL or slug in this class.

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, let me think about this some more.

Copy link
Member Author

Choose a reason for hiding this comment

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

Maybe we merge this as-is and I propose the change in another PR? I want to revisit how the URL is handled because in reality it is the request args really that matter, not the end URL. The slug is computed from the request args, not the URL.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've added this as a todo item on #869

Copy link
Member Author

Choose a reason for hiding this comment

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

Ultimately, I think it would actually make sense to store a url with each URL Metric because in actuality the URL can vary while the request args change. This could be important for debugging, for example if it turns out that a template is modified by direct access to $_GET and not for a registered query var.

But I want to address this in a subsequent PR.

Copy link
Member

Choose a reason for hiding this comment

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

Oh I see you added more comments in the meantime. Let me know what you think about the above proposal.

But in any case, this would be okay to do in a follow up PR.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, let's do in a separate PR. It would actually be useful to duplicate the URL and even the request args for each URL metric since it reveals the underlying data for the response that created the URL metric. The slug used in the post type is computed from a normalized set of request args, so having the original request args would still be useful to have on hand.

Copy link
Member

Choose a reason for hiding this comment

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

Sounds great!

Copy link
Member Author

Choose a reason for hiding this comment

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

See #1043

* @return int|WP_Error Post ID or WP_Error otherwise.
*/
function ilo_store_url_metric( string $url, string $slug, array $validated_url_metric ) {
function ilo_store_url_metric( string $url, string $slug, ILO_URL_Metric $new_url_metric ) {
Copy link
Member

Choose a reason for hiding this comment

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

See my above comment: I think a function like this could be made more intuitive if ILO_URL_Metric covered the URL and slug as well. This way you'd only pass around the object, not the other two values in addition.

Comment on lines +41 to +61
'url' => array(
'type' => 'string',
'description' => __( 'The URL for which the metric was obtained.', 'performance-lab' ),
'required' => true,
'format' => 'uri',
'validate_callback' => static function ( $url ) {
if ( ! wp_validate_redirect( $url ) ) {
return new WP_Error( 'non_origin_url', __( 'URL for another site provided.', 'performance-lab' ) );
}
// TODO: This is not validated as corresponding to the slug in any way. True it is not used for anything but metadata.
return true;
},
),
'slug' => array(
'type' => 'string',
'description' => __( 'An MD5 hash of the query args.', 'performance-lab' ),
'required' => true,
'pattern' => '^[0-9a-f]{32}$',
// This is validated via the nonce validate_callback, as it is provided as input to create the nonce by the server
// which then is verified to match in the REST API request.
),
Copy link
Member

Choose a reason for hiding this comment

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

See above, this could be tied to the ILO_URL_Metric class.

@westonruter westonruter merged commit 47a1d0a into feature/image-loading-optimization Feb 29, 2024
37 checks passed
@westonruter westonruter deleted the add/url-metric-class branch February 29, 2024 01:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Focus] Images Issues related to the Images focus area no milestone PRs that do not have a defined milestone for release [Plugin] Optimization Detective Issues for the Optimization Detective plugin [Type] Feature A new feature within an existing module
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants