Skip to content

Commit

Permalink
Merge pull request #967 from biigle/streamed-annotation-response
Browse files Browse the repository at this point in the history
Streamed annotation response
  • Loading branch information
mzur authored Nov 27, 2024
2 parents 2dbacfa + c862628 commit 47c52ad
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 46 deletions.
15 changes: 12 additions & 3 deletions app/AnnotationSession.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Carbon\Carbon;
use DB;
use Generator;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

Expand Down Expand Up @@ -64,10 +65,11 @@ public function users()
*
* @param VolumeFile $file The file to get the annotations from
* @param User $user The user to whom the restrictions should apply ('own' user)
* @param array $load Models that should also be loaded
*
* @return \Illuminate\Database\Eloquent\Collection
* @return Generator
*/
public function getVolumeFileAnnotations(VolumeFile $file, User $user)
public function getVolumeFileAnnotations(VolumeFile $file, User $user, array $load = [])
{
$annotationClass = $file->annotations()->getRelated();
$query = $annotationClass::allowedBySession($this, $user)
Expand Down Expand Up @@ -105,7 +107,14 @@ public function getVolumeFileAnnotations(VolumeFile $file, User $user)
$query->with('labels');
}

return $query->get();
// Prevent exceeding memory limit by using generator
$yieldAnnotations = function () use ($query, $load): Generator {
foreach ($query->with($load)->lazy() as $annotation) {
yield $annotation;
}
};

return $yieldAnnotations;
}

/**
Expand Down
17 changes: 13 additions & 4 deletions app/Http/Controllers/Api/ImageAnnotationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
use Biigle\Shape;
use DB;
use Exception;
use Generator;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpFoundation\StreamedJsonResponse;

class ImageAnnotationController extends Controller
{
Expand Down Expand Up @@ -60,7 +62,7 @@ class ImageAnnotationController extends Controller
*
* @param Request $request
* @param int $id image id
* @return \Illuminate\Database\Eloquent\Collection<int, ImageAnnotation>
* @return \Symfony\Component\HttpFoundation\StreamedJsonResponse
*/
public function index(Request $request, $id)
{
Expand All @@ -78,11 +80,18 @@ public function index(Request $request, $id)
'labels.user:id,firstname,lastname',
];

// Prevent exceeding memory limit by using generator and stream
if ($session) {
return $session->getVolumeFileAnnotations($image, $user)->load($load);
$yieldAnnotations = $session->getVolumeFileAnnotations($image, $user, $load);
} else {
$yieldAnnotations = function () use ($image, $load): Generator {
foreach ($image->annotations()->with($load)->lazy() as $annotation) {
yield $annotation;
}
};
}

return $image->annotations()->with($load)->get();
return new StreamedJsonResponse($yieldAnnotations());
}

/**
Expand Down
15 changes: 12 additions & 3 deletions app/Http/Controllers/Api/VideoAnnotationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
use Cache;
use DB;
use Exception;
use Generator;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Queue;
use Symfony\Component\HttpFoundation\StreamedJsonResponse;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;

class VideoAnnotationController extends Controller
Expand Down Expand Up @@ -61,7 +63,7 @@ class VideoAnnotationController extends Controller
*
* @param Request $request
* @param int $id Video id
* @return mixed
* @return \Symfony\Component\HttpFoundation\StreamedJsonResponse
*/
public function index(Request $request, $id)
{
Expand All @@ -72,11 +74,18 @@ public function index(Request $request, $id)
$session = $video->volume->getActiveAnnotationSession($user);
$load = ['labels.label', 'labels.user'];

// Prevent exceeding memory limit by using generator and stream
if ($session) {
return $session->getVolumeFileAnnotations($video, $user)->load($load);
$yieldAnnotations = $session->getVolumeFileAnnotations($video, $user, $load);
} else {
$yieldAnnotations = function () use ($video, $load): Generator {
foreach ($video->annotations()->with($load)->lazy() as $annotation) {
yield $annotation;
}
};
}

return $video->annotations()->with($load)->get();
return new StreamedJsonResponse($yieldAnnotations());
}

/**
Expand Down
32 changes: 16 additions & 16 deletions tests/php/AnnotationSessionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ public function testGetVolumeFileAnnotationsHideOwnImage()
'hide_other_users_annotations' => false,
]);

$annotations = $session->getVolumeFileAnnotations($image, $ownUser);
$yieldAnnotations = $session->getVolumeFileAnnotations($image, $ownUser);
// expand the models in the collection so we can make assertions
$annotations = collect($annotations->toArray());
$annotations = collect(collect($yieldAnnotations())->toArray());

$this->assertTrue($annotations->contains('points', [20, 30, 40]));
$this->assertFalse($annotations->contains('labels', [$al11->toArray()]));
Expand Down Expand Up @@ -169,9 +169,9 @@ public function testGetVolumeFileAnnotationsHideOwnVideo()
'hide_other_users_annotations' => false,
]);

$annotations = $session->getVolumeFileAnnotations($video, $ownUser);
$yieldAnnotations = $session->getVolumeFileAnnotations($video, $ownUser);
// expand the models in the collection so we can make assertions
$annotations = collect($annotations->toArray());
$annotations = collect(collect($yieldAnnotations())->toArray());

$this->assertTrue($annotations->contains('points', [[20, 30, 40]]));
$this->assertFalse($annotations->contains('labels', [$al11->toArray()]));
Expand Down Expand Up @@ -212,9 +212,9 @@ public function testGetVolumeFileAnnotationsHideOtherImage()
'hide_other_users_annotations' => true,
]);

$annotations = $session->getVolumeFileAnnotations($image, $ownUser);
$yieldAnnotations = $session->getVolumeFileAnnotations($image, $ownUser);
// expand the models in the collection so we can make assertions
$annotations = collect($annotations->toArray());
$annotations = collect(collect($yieldAnnotations())->toArray());

$this->assertTrue($annotations->contains('points', [20, 30, 40]));
$this->assertFalse($annotations->contains('labels', [$al1->toArray()]));
Expand Down Expand Up @@ -252,9 +252,9 @@ public function testGetVolumeFileAnnotationsHideOtherVideo()
'hide_other_users_annotations' => true,
]);

$annotations = $session->getVolumeFileAnnotations($video, $ownUser);
$yieldAnnotations = $session->getVolumeFileAnnotations($video, $ownUser);
// expand the models in the collection so we can make assertions
$annotations = collect($annotations->toArray());
$annotations = collect(collect($yieldAnnotations())->toArray());

$this->assertTrue($annotations->contains('points', [[20, 30, 40]]));
$this->assertFalse($annotations->contains('labels', [$al1->toArray()]));
Expand Down Expand Up @@ -292,9 +292,9 @@ public function testGetVolumeFileAnnotationsHideBothImage()
'hide_other_users_annotations' => true,
]);

$annotations = $session->getVolumeFileAnnotations($image, $ownUser);
$yieldAnnotations = $session->getVolumeFileAnnotations($image, $ownUser);
// expand the models in the collection so we can make assertions
$annotations = collect($annotations->toArray());
$annotations = collect(collect($yieldAnnotations())->toArray());

$this->assertTrue($annotations->contains('points', [40, 50, 60]));
$this->assertTrue($annotations->contains('labels', [$al1->toArray()]));
Expand Down Expand Up @@ -332,9 +332,9 @@ public function testGetVolumeFileAnnotationsHideBothVideo()
'hide_other_users_annotations' => true,
]);

$annotations = $session->getVolumeFileAnnotations($video, $ownUser);
$yieldAnnotations = $session->getVolumeFileAnnotations($video, $ownUser);
// expand the models in the collection so we can make assertions
$annotations = collect($annotations->toArray());
$annotations = collect(collect($yieldAnnotations())->toArray());

$this->assertTrue($annotations->contains('points', [[40, 50, 60]]));
$this->assertTrue($annotations->contains('labels', [$al1->toArray()]));
Expand Down Expand Up @@ -371,9 +371,9 @@ public function testGetVolumeFileAnnotationsHideNothingImage()
'hide_other_users_annotations' => false,
]);

$annotations = $session->getVolumeFileAnnotations($image, $ownUser);
$yieldAnnotations = $session->getVolumeFileAnnotations($image, $ownUser);
// expand the models in the collection so we can make assertions
$annotations = collect($annotations->toArray());
$annotations = collect(collect($yieldAnnotations())->toArray());

$this->assertTrue($annotations->contains('points', [40, 50, 60]));
$this->assertTrue($annotations->contains('labels', [
Expand Down Expand Up @@ -412,9 +412,9 @@ public function testGetVolumeFileAnnotationsHideNothingVideo()
'hide_other_users_annotations' => false,
]);

$annotations = $session->getVolumeFileAnnotations($video, $ownUser);
$yieldAnnotations = $session->getVolumeFileAnnotations($video, $ownUser);
// expand the models in the collection so we can make assertions
$annotations = collect($annotations->toArray());
$annotations = collect(collect($yieldAnnotations())->toArray());

$this->assertTrue($annotations->contains('points', [[40, 50, 60]]));
$this->assertTrue($annotations->contains('labels', [
Expand Down
56 changes: 47 additions & 9 deletions tests/php/Http/Controllers/Api/ImageAnnotationControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
use Biigle\Tests\LabelTest;
use Cache;
use Carbon\Carbon;
use Illuminate\Testing\TestResponse;
use Symfony\Component\HttpFoundation\Response;

class ImageAnnotationControllerTest extends ApiTestCase
{
Expand Down Expand Up @@ -49,11 +51,23 @@ public function testIndex()
$response->assertStatus(403);

$this->beGuest();
$response = $this->get("/api/v1/images/{$this->image->id}/annotations")
->assertJsonFragment(['points' => [10, 20, 30, 40]])
$response = $this->getJson("/api/v1/images/{$this->image->id}/annotations")->assertStatus(200);

ob_start();
$response->sendContent();
$content = ob_get_clean();
$response = new TestResponse(
new Response(
$content,
$response->baseResponse->getStatusCode(),
$response->baseResponse->headers->all()
)
);

$response->assertJsonFragment(['points' => [10, 20, 30, 40]])
->assertJsonFragment(['color' => 'bada55'])
->assertJsonFragment(['name' => 'My label']);
$response->assertStatus(200);

}

public function testIndexAnnotationSessionHideOwn()
Expand Down Expand Up @@ -89,18 +103,42 @@ public function testIndexAnnotationSessionHideOwn()
]);

$this->beEditor();
$response = $this->get("/api/v1/images/{$this->image->id}/annotations")
->assertJsonFragment(['points' => [10, 20]])
$response = $this->getJson("/api/v1/images/{$this->image->id}/annotations")->assertStatus(200);

ob_start();
$response->sendContent();
$content = ob_get_clean();
$response = new TestResponse(
new Response(
$content,
$response->baseResponse->getStatusCode(),
$response->baseResponse->headers->all()
)
);

$response->assertJsonFragment(['points' => [10, 20]])
->assertJsonFragment(['points' => [20, 30]]);
$response->assertStatus(200);


$session->users()->attach($this->editor());
Cache::flush();

$response = $this->get("/api/v1/images/{$this->image->id}/annotations")
->assertJsonMissing(['points' => [10, 20]])
$response = $this->getJson("/api/v1/images/{$this->image->id}/annotations")->assertStatus(200);

ob_start();
$response->sendContent();
$content = ob_get_clean();
$response = new TestResponse(
new Response(
$content,
$response->baseResponse->getStatusCode(),
$response->baseResponse->headers->all()
)
);

$response->assertJsonMissing(['points' => [10, 20]])
->assertJsonFragment(['points' => [20, 30]]);
$response->assertStatus(200);

}

public function testShow()
Expand Down
60 changes: 49 additions & 11 deletions tests/php/Http/Controllers/Api/VideoAnnotationControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
use Biigle\Tests\VideoTest;
use Cache;
use Carbon\Carbon;
use Illuminate\Testing\TestResponse;
use Queue;
use Symfony\Component\HttpFoundation\Response;

class VideoAnnotationControllerTest extends ApiTestCase
{
Expand Down Expand Up @@ -54,10 +56,22 @@ public function testIndex()
->assertStatus(403);

$this->beGuest();
$this
$response = $this
->getJson("/api/v1/videos/{$this->video->id}/annotations")
->assertStatus(200)
->assertJsonFragment(['frames' => [1.0]])
->assertStatus(200);

ob_start();
$response->sendContent();
$content = ob_get_clean();
$response = new TestResponse(
new Response(
$content,
$response->baseResponse->getStatusCode(),
$response->baseResponse->headers->all()
)
);

$response->assertJsonFragment(['frames' => [1.0]])
->assertJsonFragment(['points' => [[10, 20]]])
->assertJsonFragment(['color' => 'bada55'])
->assertJsonFragment(['name' => 'My label']);
Expand Down Expand Up @@ -96,19 +110,43 @@ public function testIndexAnnotationSessionHideOwn()
]);

$this->beEditor();
$this
->get("/api/v1/videos/{$this->video->id}/annotations")
->assertStatus(200)
->assertJsonFragment(['points' => [[10, 20]]])
$response = $this
->getJson("/api/v1/videos/{$this->video->id}/annotations")
->assertStatus(200);

ob_start();
$response->sendContent();
$content = ob_get_clean();
$response = new TestResponse(
new Response(
$content,
$response->baseResponse->getStatusCode(),
$response->baseResponse->headers->all()
)
);

$response->assertJsonFragment(['points' => [[10, 20]]])
->assertJsonFragment(['points' => [[20, 30]]]);

$session->users()->attach($this->editor());
Cache::flush();

$this
->get("/api/v1/videos/{$this->video->id}/annotations")
->assertStatus(200)
->assertJsonMissing(['points' => [[10, 20]]])
$response = $this
->getJson("/api/v1/videos/{$this->video->id}/annotations")
->assertStatus(200);

ob_start();
$response->sendContent();
$content = ob_get_clean();
$response = new TestResponse(
new Response(
$content,
$response->baseResponse->getStatusCode(),
$response->baseResponse->headers->all()
)
);

$response->assertJsonMissing(['points' => [[10, 20]]])
->assertJsonFragment(['points' => [[20, 30]]]);
}

Expand Down

0 comments on commit 47c52ad

Please sign in to comment.