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

Speed up annotation list #7410

Merged
merged 29 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
43af02d
Add compact annotation list query
frcroth Oct 25, 2023
c9ce6aa
remove unused function
philippotto Nov 2, 2023
b3f57bc
rename APIAnnotationCompact to APIAnnotationInfo
philippotto Nov 2, 2023
3c350d7
Merge branch 'master' into compact-annotation-list-query
frcroth Nov 6, 2023
7f6923d
Add missing fields and teams array
frcroth Nov 6, 2023
ee89fd5
Remove unused route and formattedHash
frcroth Nov 6, 2023
a8530b8
Update e2e tests
frcroth Nov 6, 2023
9050953
start integration of APIAnnotationInfoCompact
philippotto Nov 13, 2023
3a3f9ff
Merge branch 'compact-annotation-list-query' of github.com:scalablemi…
philippotto Nov 14, 2023
522b19f
remove formattedHash everywhere (also from Task); fix missing state i…
philippotto Nov 14, 2023
aad01bb
prepare merge of APIAnnotationInfo and APIAnnotationInfoCompact
philippotto Nov 14, 2023
66754a6
remove APIAnnotationInfo in favor of APIAnnotationInfoCompact
philippotto Nov 14, 2023
fb0b0cb
Merge branch 'master' into compact-annotation-list-query
frcroth Nov 15, 2023
dfa9e06
Add all values to annotationcompactinfo
frcroth Nov 15, 2023
1c5446a
Unify compact and non-compact
frcroth Nov 15, 2023
b334e40
Merge branch 'master' into compact-annotation-list-query
frcroth Nov 27, 2023
5f504d4
re-add organization and annotationLayers to APIAnnotationInfoCompact
philippotto Nov 27, 2023
e314f39
rename APIAnnotationInfoCompact to APIAnnotationInfo
philippotto Nov 27, 2023
b3bf55d
avoid unnecessary fetch of annotation object when downloading volume …
philippotto Nov 27, 2023
c21b7f4
Merge branch 'compact-annotation-list-query' of github.com:scalablemi…
philippotto Nov 27, 2023
b9b9d70
remove unused compact param
philippotto Nov 27, 2023
b42b13d
Update changelog
frcroth Nov 27, 2023
e3562b7
fix linting
philippotto Nov 27, 2023
de06cc3
Merge branch 'compact-annotation-list-query' of github.com:scalablemi…
philippotto Nov 27, 2023
d718909
Address review feedback
frcroth Nov 27, 2023
1567bf5
Merge branch 'master' into compact-annotation-list-query
fm3 Dec 4, 2023
4878996
refresh snapshots
fm3 Dec 4, 2023
d3cc826
Merge branch 'master' into compact-annotation-list-query
frcroth Dec 6, 2023
3256617
Merge branch 'master' into compact-annotation-list-query
frcroth Dec 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
### Changed
- An appropriate error is returned when requesting an API version that is higher that the current version. [#7424](https://github.com/scalableminds/webknossos/pull/7424)
- Upgraded FossilDB database used to store annotation data to version 0.1.27. [#7440](https://github.com/scalableminds/webknossos/pull/7440)
- Improved loading speed of the annotation list. [#7410](https://github.com/scalableminds/webknossos/pull/7410)

### Fixed
- Searching the segments in the sidebar will highlight newly focused segments properly now. [#7406](https://github.com/scalableminds/webknossos/pull/7406)
Expand Down
18 changes: 6 additions & 12 deletions app/controllers/AnnotationController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -595,31 +595,25 @@ class AnnotationController @Inject()(
includeTotalCount: Option[Boolean] = None): Action[AnyContent] =
sil.SecuredAction.async { implicit request =>
for {
readableAnnotations <- annotationDAO.findAllListableExplorationals(
annotationInfos <- annotationDAO.findAllListableExplorationals(
isFinished,
None,
AnnotationType.Explorational,
limit.getOrElse(annotationService.DefaultAnnotationListLimit),
pageNumber.getOrElse(0))
annotationCount <- Fox.runIf(includeTotalCount.getOrElse(false))(
annotationDAO.countAllListableExplorationals(isFinished)) ?~> "annotation.countReadable.failed"
jsonList <- Fox.serialCombined(readableAnnotations)(annotationService.compactWrites) ?~> "annotation.compactWrites.failed"
annotationInfosJsons = annotationInfos.map(annotationService.writeCompactInfo)
_ = userDAO.updateLastActivity(request.identity._id)(GlobalAccessContext)
} yield {
val result = Ok(Json.toJson(jsonList))
val result = Ok(Json.toJson(annotationInfosJsons))
annotationCount match {
case Some(count) => result.withHeaders("X-Total-Count" -> count.toString)
case None => result
}
}
}

@ApiOperation(hidden = true, value = "")
def sharedAnnotations: Action[AnyContent] = sil.SecuredAction.async { implicit request =>
for {
userTeams <- userService.teamIdsFor(request.identity._id)
sharedAnnotations <- annotationService.sharedAnnotationsFor(userTeams)
json <- Fox.serialCombined(sharedAnnotations)(annotationService.compactWrites)
} yield Ok(Json.toJson(json))
}
}

@ApiOperation(hidden = true, value = "")
def getSharedTeams(typ: String, id: String): Action[AnyContent] = sil.SecuredAction.async { implicit request =>
Expand Down
27 changes: 15 additions & 12 deletions app/controllers/UserController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,16 @@ class UserController @Inject()(userService: UserService,
includeTotalCount: Option[Boolean] = None): Action[AnyContent] =
sil.SecuredAction.async { implicit request =>
for {
annotations <- annotationDAO.findAllFor(request.identity._id,
isFinished,
AnnotationType.Explorational,
limit.getOrElse(annotationService.DefaultAnnotationListLimit),
pageNumber.getOrElse(0))
annotations <- annotationDAO.findAllListableExplorationals(
isFinished,
Some(request.identity._id),
AnnotationType.Explorational,
limit.getOrElse(annotationService.DefaultAnnotationListLimit),
pageNumber.getOrElse(0)
)
annotationCount: Option[Int] <- Fox.runIf(includeTotalCount.getOrElse(false))(
annotationDAO.countAllFor(request.identity._id, isFinished, AnnotationType.Explorational))
jsonList <- Fox.serialCombined(annotations)(a => annotationService.compactWrites(a))
jsonList = annotations.map(annotationService.writeCompactInfo)
_ = userDAO.updateLastActivity(request.identity._id)(GlobalAccessContext)
} yield {
val result = Ok(Json.toJson(jsonList))
Expand Down Expand Up @@ -179,14 +181,15 @@ class UserController @Inject()(userService: UserService,
userIdValidated <- ObjectId.fromString(userId) ?~> "user.id.invalid"
user <- userDAO.findOne(userIdValidated) ?~> "user.notFound" ~> NOT_FOUND
_ <- Fox.assertTrue(userService.isEditableBy(user, request.identity)) ?~> "notAllowed" ~> FORBIDDEN
annotations <- annotationDAO.findAllFor(userIdValidated,
isFinished,
AnnotationType.Explorational,
limit.getOrElse(annotationService.DefaultAnnotationListLimit),
pageNumber.getOrElse(0))
annotations <- annotationDAO.findAllListableExplorationals(
isFinished,
Some(userIdValidated),
AnnotationType.Explorational,
limit.getOrElse(annotationService.DefaultAnnotationListLimit),
pageNumber.getOrElse(0))
annotationCount <- Fox.runIf(includeTotalCount.getOrElse(false))(
annotationDAO.countAllFor(userIdValidated, isFinished, AnnotationType.Explorational))
jsonList <- Fox.serialCombined(annotations)(annotationService.compactWrites)
jsonList = annotations.map(annotationService.writeCompactInfo)
} yield {
val result = Ok(Json.toJson(jsonList))
annotationCount match {
Expand Down
160 changes: 144 additions & 16 deletions app/models/annotation/Annotation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,33 @@ case class Annotation(

}

case class AnnotationCompactInfo(id: ObjectId,
typ: AnnotationType.Value,
name: String,
description: String,
ownerId: ObjectId,
ownerFirstName: String,
ownerLastName: String,
othersMayEdit: Boolean,
teamIds: Seq[ObjectId],
teamNames: Seq[String],
teamOrganizationIds: Seq[ObjectId],
modified: Instant,
stats: JsObject,
tags: Set[String],
state: AnnotationState.Value = Active,
dataSetName: String,
visibility: AnnotationVisibility.Value = AnnotationVisibility.Internal,
tracingTime: Option[Long] = None,
organizationName: String,
tracingIds: Seq[String],
annotationLayerNames: Seq[String],
annotationLayerTypes: Seq[String])

object AnnotationCompactInfo {
implicit val jsonFormat: Format[AnnotationCompactInfo] = Json.format[AnnotationCompactInfo]
}

class AnnotationLayerDAO @Inject()(SQLClient: SqlClient)(implicit ec: ExecutionContext)
extends SimpleSQLDAO(SQLClient) {

Expand Down Expand Up @@ -193,22 +220,22 @@ class AnnotationDAO @Inject()(sqlClient: SqlClient, annotationLayerDAO: Annotati
override protected def anonymousReadAccessQ(sharingToken: Option[String]) =
q"visibility = ${AnnotationVisibility.Public}"

private def listAccessQ(requestingUserId: ObjectId): SqlToken =
private def listAccessQ(requestingUserId: ObjectId, prefix: SqlToken): SqlToken =
q"""
(
_user = $requestingUserId
or (
(visibility = ${AnnotationVisibility.Public} or visibility = ${AnnotationVisibility.Internal})
(${prefix}visibility = ${AnnotationVisibility.Public} or ${prefix}visibility = ${AnnotationVisibility.Internal})
and (
_id in (
${prefix}_id in (
select distinct a._annotation
from webknossos.annotation_sharedTeams a
join webknossos.user_team_roles t
on a._team = t._team
where t._user = $requestingUserId
)
or
_id in (
${prefix}_id in (
select _annotation from webknossos.annotation_contributors
where _user = $requestingUserId
)
Expand All @@ -217,6 +244,9 @@ class AnnotationDAO @Inject()(sqlClient: SqlClient, annotationLayerDAO: Annotati
)
"""

private def baseListAccessQ(implicit ctx: DBAccessContext): Fox[SqlToken] =
accessQueryFromAccessQWithPrefix(listAccessQ, q"")(ctx)

override protected def readAccessQ(requestingUserId: ObjectId): SqlToken =
q"""(
visibility = ${AnnotationVisibility.Public}
Expand Down Expand Up @@ -268,22 +298,120 @@ class AnnotationDAO @Inject()(sqlClient: SqlClient, annotationLayerDAO: Annotati
} yield parsed
}

def findAllListableExplorationals(isFinished: Option[Boolean], limit: Int, pageNumber: Int = 0)(
implicit ctx: DBAccessContext): Fox[List[Annotation]] = {
val stateQuery = getStateQuery(isFinished)
for {
accessQuery <- accessQueryFromAccessQ(listAccessQ)
r <- run(q"""select $columns from $existingCollectionName
where typ = ${AnnotationType.Explorational} and $stateQuery and $accessQuery
order by _id desc limit $limit offset ${pageNumber * limit}""".as[AnnotationsRow])
parsed <- parseAll(r)
} yield parsed
}
private def parseObjectIdArray(objectIdArray: String): Seq[ObjectId] =
Option(objectIdArray).map(_.split(",").map(id => ObjectId(id))).getOrElse(Array[ObjectId]()).toSeq
fm3 marked this conversation as resolved.
Show resolved Hide resolved

def findAllListableExplorationals(
isFinished: Option[Boolean],
forUser: Option[ObjectId],
typ: AnnotationType,
limit: Int,
pageNumber: Int = 0)(implicit ctx: DBAccessContext): Fox[List[AnnotationCompactInfo]] =
for {
accessQuery <- accessQueryFromAccessQWithPrefix(listAccessQ, q"a.")
stateQuery = getStateQuery(isFinished)
userQuery = forUser.map(u => q"a._user = $u").getOrElse(q"true")
typQuery = q"a.typ = $typ"

query = q"""
SELECT
a._id,
a.name,
a.description,
a._user,
u.firstname,
u.lastname,
a.othersmayedit,
STRING_AGG(t._id, ',') AS team_ids,
STRING_AGG(t.name, ',') AS team_names,
STRING_AGG(t._organization, ',') AS team_orgs,
a.modified,
a.statistics,
a.tags,
a.state,
d.name,
a.typ,
a.visibility,
a.tracingtime,
o.name,
STRING_AGG(al.tracingid, ',') AS tracing_ids,
STRING_AGG(al.name, ',') AS tracing_names,
STRING_AGG(al.typ :: varchar, ',') AS tracing_typs
FROM webknossos.annotations as a
LEFT JOIN webknossos.users_ u
ON u._id = a._user
LEFT JOIN webknossos.annotation_sharedteams ast
ON ast._annotation = a._id
LEFT JOIN webknossos.teams_ t
ON ast._team = t._id
LEFT JOIN webknossos.datasets_ d
ON d._id = a._dataset
LEFT JOIN webknossos.organizations_ as o
ON o._id = d._organization
LEFT JOIN webknossos.annotation_layers as al
ON al._annotation = a._id
WHERE $stateQuery AND $accessQuery AND $userQuery AND $typQuery
GROUP BY a._id, u.firstname, u.lastname, d.name, o.name
ORDER BY a._id DESC LIMIT $limit OFFSET ${pageNumber * limit}
"""
rows <- run(
query.as[
(ObjectId,
String,
String,
ObjectId,
String,
String,
Boolean,
String,
String,
String,
Instant,
String,
String,
String,
String,
String,
String,
Long,
String,
String,
String,
String)])
} yield
rows.toList.map(
r => {
AnnotationCompactInfo(
id = r._1,
name = r._2,
description = r._3,
ownerId = r._4,
ownerFirstName = r._5,
ownerLastName = r._6,
othersMayEdit = r._7,
teamIds = parseObjectIdArray(r._8),
teamNames = Option(r._9).map(_.split(",")).getOrElse(Array[String]()).toSeq,
fm3 marked this conversation as resolved.
Show resolved Hide resolved
teamOrganizationIds = parseObjectIdArray(r._10),
modified = r._11,
stats = Json.parse(r._12).validate[JsObject].getOrElse(Json.obj()),
tags = parseArrayLiteral(r._13).toSet,
state = AnnotationState.fromString(r._14).getOrElse(AnnotationState.Active),
dataSetName = r._15,
typ = AnnotationType.fromString(r._16).getOrElse(AnnotationType.Explorational),
visibility = AnnotationVisibility.fromString(r._17).getOrElse(AnnotationVisibility.Internal),
tracingTime = Option(r._18),
organizationName = r._19,
tracingIds = Option(r._20).map(_.split(",")).getOrElse(Array[String]()).toSeq,
annotationLayerNames = Option(r._21).map(_.split(",")).getOrElse(Array[String]()).toSeq,
annotationLayerTypes = Option(r._22).map(_.split(",")).getOrElse(Array[String]()).toSeq
)
}
)

def countAllListableExplorationals(isFinished: Option[Boolean])(implicit ctx: DBAccessContext): Fox[Long] = {
val stateQuery = getStateQuery(isFinished)
for {
accessQuery <- accessQueryFromAccessQ(listAccessQ)
accessQuery <- baseListAccessQ
rows <- run(q"""select count(_id) from $existingCollectionName
where typ = ${AnnotationType.Explorational} and ($stateQuery) and ($accessQuery)""".as[Long])
count <- rows.headOption.toFox
Expand Down
Loading