Spring HATEOAS ControllerLinkBuilder methodOn increasing response times significantly
Asked Answered
P

1

3

The setup: So I have a RESTfull API written in java, using spring-boot and spring-hates for adding links to the resource (Hypermedia-Driven RESTful Web Service). Everything I have is standard and no additional settings or changes are made

The problem

  1. Case: no links on resource - Chrome TTFB avg. (10 runs) 400ms for 1000 items
  2. Case: 1 self reference link on resource - Chrome TTFB avg. (10 runs) 1500ms for 1000 items

I am using this official guide

The question

Why adding only 1 link to my resource adds additional 1 second for the processing the request. I will need around 5 - 7 links on each resource and every resource has additional embedded ones?

For 9000 total items with only 1 link per item (included the nested ones), i have to wait 30 sec for the response and without links ~ 400 ms.

P.S. The additional code is irrelevant because I am just adding a code from the tutorial which effects the performance dramatically.

Edit 1

As suggested I am adding example code from my TextItem constructor

add(linkTo(methodOn(TestController.class).getTestItems()).withRel("testLink"));

Edit 2

So the following example proposed from @Mathias Dpunkt works absolutely perfect

private Method method = ReflectionUtils.findMethod(TestController.class, "getOne", Integer.class);

@Override
public Resource<Item> process(Resource<Item> resource) {
  resource.add(linkTo(method, resource.getContent().getId()).withSelfRel());
  return resource;
}

The new problem

Controller:

@RestController
@RequestMapping("items")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class TestController {

    private final ItemResourceProcessor resourceProcessor;

    @RequestMapping(method = GET)
    public ResponseEntity<List<Resource<Item>>> getAll() {
        List<Resource<Item>> items = new ArrayList<>(100);
        for (int i = 0; i < 100; i++) {
            items.add(resourceProcessor.process(
                    new Resource<>(new Item(i, UUID.randomUUID().toString()))));
        }

        return ResponseEntity.ok(items);
    }

    @RequestMapping(method = GET, path = "/{id}")
    public ResponseEntity<Resource<Item>> getOne(@PathVariable Integer id, @RequestParam boolean test1, @RequestParam boolean test2) {
        return null;
    }
}

If the controller method takes @RequestParam the posted solution does not append it to the link. When I call

private Method method = ReflectionUtils.findMethod(TestController.class, "getOne", Integer.class);    

@Override
public Resource<Item> process(Resource<Item> resource) {
     resource.add(linkTo(method, resource.getContent().getId(), true, true).withSelfRel());
     return resource;
}
Pieter answered 30/3, 2016 at 8:35 Comment(8)
Profile your application.Lampley
@Lampley even that my question is based on specific app - the case is something that happens on every spring application which I have - I detected it just now when the response size exploded, but that is not the problem, isn`t it..Pieter
Adding a link doesn't cause the problem. The slowdown is more likely due to using methodOn. But as long as you believe that it's not necessary to show your code we can't help you.Medicable
@zeroflagL I edited the postPieter
Try linkTo(TestController.class).withRel("testLink"). If necessary you can add an ID (or another path segment) using linkTo(TestController.class).slash(id). methodOn is quite costly.Medicable
@DanielDonev Next time you get a suggestion that doesn't cost any money and very little time, I recommend trying it out instead of commenting how you don't believe it will help. You can still salvage this question by creating a good self-answer.Lampley
@Lampley I could spend quite some time explaining why running a profiler on my app won't help me, but I have better stuff to do so I would try the solution from zeroflagL. And one more thing - for future advices as the last one, which does not help rather than just stating the obvious - try another website ;)Pieter
@DanielDonev If the issue here does turn out to be methodOn() being expensive, then a profiler would show it. You're saying that you don't have the "time" to explain, but you would've saved a lot more time by taking my advice immediately in that case. If you can't explain why profiling is out of the question, then I can only assume that you don't really understand what I was suggesting. After all, it's the first thing to do if you have performance problems.Lampley
M
14

EDIT:

Updated post to use improved versions - Spring HATEOAS 0.22 and Spring Framework 4.3.5 / 5.0 M4.

Answer:

This is very interesting. I had a look at the source code for the ControllerLinkBuilder methods linkToand methodOn and there is a lot going on for a simple link:

  • builds an aop propxy for the controller that records the interactions and gets hold of the method and the parameters to build the link for
  • it discovers the mappings of this method to construct the link

The ControllerLinkBuilder is very convenient because it avoids duplicating logic that is already contained in your mapping.

I came up with a simple sample application and a very basic benchmark to measure and compare link builder performance

It is based on a simple controller - it is just returning 100 simple objects - each is carrying one self-link.

@RestController
@RequestMapping("items")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class TestController {

    private final ItemResourceProcessor resourceProcessor;

    @RequestMapping(method = GET)
    public ResponseEntity<List<Resource<Item>>> getAll() {
        List<Resource<Item>> items = new ArrayList<>(100);
        for (int i = 0; i < 100; i++) {
            items.add(resourceProcessor.process(
                    new Resource<>(new Item(i, UUID.randomUUID().toString()))));
        }

        return ResponseEntity.ok(items);
    }

    @RequestMapping(method = GET, path = "/{id}")
    public ResponseEntity<Resource<Item>> getOne(@PathVariable Integer id) {
        return null;
    }
}

The ItemResourceProcessor adds a simple self-link and I tried and measured three different alternatives:

1. ControllerLinkBuilder with linkTo(methodOn)

Here ControllerLinkBuilder is used to inspect the mapping on controller and method - which needs an aop proxy for every link generated.

@Component
public class ItemResourceProcessor implements ResourceProcessor<Resource<Item>> {

    @Override
    public Resource<Item> process(Resource<Item> resource) {
        resource.add(linkTo(methodOn(TestController.class).getOne(resource.getContent().getId())).withSelfRel());
        return resource;
    }
}

The results for this variant are these:

    wrk -t2 -c5 -d30s http://localhost:8080/items

    Running 30s test @ http://localhost:8080/items
  2 threads and 5 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     4.77ms    0.93ms  25.57ms   83.97%
    Req/Sec   420.87     48.63   500.00     71.33%
  25180 requests in 30.06s, 305.70MB read
Requests/sec:    837.63

2. ControllerLinkBuilder without methodOn()

Here the call the to methodOn() is avoided and the method reference is determined once at the creation of the resource processor and reused to generate the link. This version avoids the overhead of methodOn but still discovers the mapping on the method to generate the link.

@Component
public class ItemResourceProcessor implements ResourceProcessor<Resource<Item>> {

    private Method method = ReflectionUtils.findMethod(TestController.class, "getOne", Integer.class);

    @Override
    public Resource<Item> process(Resource<Item> resource) {
    resource.add(linkTo(method, resource.getContent().getId()).withSelfRel());
    return resource;
    }
}

The results are slightly better than for the first version. The optimization is giving us only small benefits.

wrk -t2 -c5 -d30s http://localhost:8080/items

Running 30s test @ http://localhost:8080/items
  2 threads and 5 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     4.02ms  477.64us  13.80ms   84.01%
    Req/Sec   499.42     18.24   540.00     65.50%
  29871 requests in 30.05s, 365.50MB read
Requests/sec:    994.03

3. Link generation using BasicLinkBuilder

Here we move away from ControllerLinkBuilder and use BasicLinkBuilder. This implementation is not performing any introspections of controller mappings and is thus a good candidate for reference benchmark.

@Component
public class ItemResourceProcessor implements ResourceProcessor<Resource<Item>> {

    private ControllerLinkBuilder baseLink;

    @Override
    public Resource<Item> process(Resource<Item> resource) {
      resource.add(BasicLinkBuilder.linkToCurrentMapping()
            .slash("items")
            .slash(resource.getContent().getId()).withSelfRel());
      return resource;
    }
}

The results are again better than the previous

wrk -t2 -c5 -d30s http://localhost:8080/items

Running 30s test @ http://localhost:8080/items
  2 threads and 5 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     3.05ms  683.71us  12.84ms   72.12%
    Req/Sec   658.31     87.79   828.00     66.67%
  39349 requests in 30.03s, 458.91MB read
Requests/sec:   1310.14

Summary

Of course, there is an overhead of methodOn(). The tests show that 100 links cost us less than 2ms on average compared to BasicLinkBuilder.

So when the amounts of rendered links are not massive the convenience of ControllerLinkBuilder is making it a good choice for link generation.

DISCLAIMER: I know my wrk tests are not proper benchmarks - but the results could be repeated and showed the same results comparing the alternatives - so they at least can provide a hint on the dimensions of differences in performance)

Maice answered 31/3, 2016 at 7:15 Comment(9)
I will try it ASAP :) Thank you for the proposalPieter
I am sorry but i could`t test it yet, but your result seems promising. I will try to find time to do it in the weekend and will share my results based on the same setup, used when I posed the question. Thank you again for the help :)Pieter
I have performed some testing and the results are very similar to yours and very promessing. But if i have @PathVariable in my method, it does not appear as url parameter as in the methodTo(). Any idea why is like that ?Pieter
Please make sure those things are not just documented as SO answers but also brought to the attention of the project maintainers to fix. In the meantime this seems to be filed and fixed in Spring HATEOAS and Spring Framework.Valuable
@OliverGierke thanks for the hint - actually I did not realize that the performance of the ControllerLinkBuilder is due to a bug - I thought it was due to proxy creation overhead that could no be avoided.Maice
@OliverGierke I will update the results with the improved version when there is a spring-boot snapshot that includes the new versions.Maice
@MathiasDpunkt — Spring Boot 1.4.3 is out upgrading to Spring 4.3.5 which should give a significant reduction in times here as the main issue was fixed there. You should some additional improvements by upgrading to Spring HATEOAS 0.23.Valuable
@OliverGierke I have that on the list - updated numbers will be added shortly.Maice
@OliverGierke updated my measurements using spring versions containing improvements for this particular aspectMaice

© 2022 - 2024 — McMap. All rights reserved.