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

🛠️ Zones | Add LAST_ZONE and Remove N+1 for zones queries #11017

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

thibpoullain
Copy link

@thibpoullain thibpoullain commented Nov 5, 2024

Bonjour !

J'ai dû résoudre des problématiques liées aux zones sur démarches sociales, et afin de réduire le delta de codebase je me demandais si ce que j'ai fait pouvait vous intéresser.

Nous avons une zone "Autre" que nous souhations mettre à la fin de la liste dans app/views/administrateurs/procedures/zones.html.haml

J'ai donc modifié le model Zones pour définir dans la codebase la zone à mettre à la fin, et j'ai modifié le controlleur pour faire le travail de tri.

J'ai amélioré le test du controlleur pour consolider le tri.

J'en ai profité pour résoudre un soucis de N+1 générées par la méthode self.available_at qui sortait :

ZoneLabel Load (8.4ms)  SELECT "zone_labels".* FROM "zone_labels" WHERE "zone_labels"."zone_id" = $1 AND (designated_on < '2024-11-05 08:56:14.401049') ORDER BY "zone_labels"."designated_on" DESC LIMIT $2  [["zone_id", 1], ["LIMIT", 1]]
  ↳ app/models/zone.rb:22:in `label_at'
  ZoneLabel Load (1.6ms)  SELECT "zone_labels".* FROM "zone_labels" WHERE "zone_labels"."zone_id" = $1 AND (designated_on < '2024-11-05 08:56:14.401049') ORDER BY "zone_labels"."designated_on" DESC LIMIT $2  [["zone_id", 2], ["LIMIT", 1]]
  ↳ app/models/zone.rb:22:in `label_at'
  ZoneLabel Load (1.3ms)  SELECT "zone_labels".* FROM "zone_labels" WHERE "zone_labels"."zone_id" = $1 AND (designated_on < '2024-11-05 08:56:14.401049') ORDER BY "zone_labels"."designated_on" DESC LIMIT $2  [["zone_id", 3], ["LIMIT", 1]]
  ↳ app/models/zone.rb:22:in `label_at'
  ZoneLabel Load (1.6ms)  SELECT "zone_labels".* FROM "zone_labels" WHERE "zone_labels"."zone_id" = $1 AND (designated_on < '2024-11-05 08:56:14.401049') ORDER BY "zone_labels"."designated_on" DESC LIMIT $2  [["zone_id", 4], ["LIMIT", 1]]
  ↳ app/models/zone.rb:22:in `label_at'
  ZoneLabel Load (1.6ms)  SELECT "zone_labels".* FROM "zone_labels" WHERE "zone_labels"."zone_id" = $1 AND (designated_on < '2024-11-05 08:56:14.401049') ORDER BY "zone_labels"."designated_on" DESC LIMIT $2  [["zone_id", 5], ["LIMIT", 1]]
  ↳ app/models/zone.rb:22:in `label_at'
  ZoneLabel Load (1.3ms)  SELECT "zone_labels".* FROM "zone_labels" WHERE "zone_labels"."zone_id" = $1 AND (designated_on < '2024-11-05 08:56:14.401049') ORDER BY "zone_labels"."designated_on" DESC LIMIT $2  [["zone_id", 6], ["LIMIT", 1]]
  ↳ app/models/zone.rb:22:in `label_at'
  ZoneLabel Load (1.0ms)  SELECT "zone_labels".* FROM "zone_labels" WHERE "zone_labels"."zone_id" = $1 AND (designated_on < '2024-11-05 08:56:14.401049') ORDER BY "zone_labels"."designated_on" DESC LIMIT $2  [["zone_id", 7], ["LIMIT", 1]]
  ↳ app/models/zone.rb:22:in `label_at'
  ZoneLabel Load (1.0ms)  SELECT "zone_labels".* FROM "zone_labels" WHERE "zone_labels"."zone_id" = $1 AND (designated_on < '2024-11-05 08:56:14.401049') ORDER BY "zone_labels"."designated_on" DESC LIMIT $2  [["zone_id", 8], ["LIMIT", 1]]
  ↳ app/models/zone.rb:22:in `label_at'
  ZoneLabel Load (1.1ms)  SELECT "zone_labels".* FROM "zone_labels" WHERE "zone_labels"."zone_id" = $1 AND (designated_on < '2024-11-05 08:56:14.401049') ORDER BY "zone_labels"."designated_on" DESC LIMIT $2  [["zone_id", 9], ["LIMIT", 1]]
  ↳ app/models/zone.rb:22:in `label_at'
...

Merci à vous ! N'hésitez pas à me faire un retour ! :)

Copy link

codecov bot commented Nov 5, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 84.49%. Comparing base (d61078d) to head (3a15a7f).
Report is 103 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main   #11017   +/-   ##
=======================================
  Coverage   84.48%   84.49%           
=======================================
  Files        1164     1163    -1     
  Lines       25691    25747   +56     
  Branches     4862     4875   +13     
=======================================
+ Hits        21705    21754   +49     
- Misses       3986     3993    +7     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

available_zones = zones.filter { |zone| zone_labels[zone.id] != 'Non attribué' }

available_zones.map do |zone|
OpenStruct.new(id: zone.id, label: zone_labels[zone.id])
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
OpenStruct.new(id: zone.id, label: zone_labels[zone.id])
LabelModel.new(id: zone.id, label: zone_labels[zone.id])

@thibpoullain thibpoullain force-pushed the zones_improvement branch 2 times, most recently from 77ea7c1 to 19fb7de Compare November 5, 2024 18:38
Copy link
Member

@LeSim LeSim left a comment

Choose a reason for hiding this comment

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

voici qq pistes d'amélioration.

Faut que je vois avec tout le monde si l'inscription de la variable autre est possible alors que nous ne l'utilisons pas.

@@ -5,23 +5,32 @@ class Zone < ApplicationRecord
has_many :labels, -> { order(designated_on: :desc) }, class_name: 'ZoneLabel', inverse_of: :zone
has_and_belongs_to_many :procedures, -> { order(published_at: :desc) }, inverse_of: :zone

LAST_ZONE = 'Autre'.freeze
Copy link
Member

@LeSim LeSim Nov 5, 2024

Choose a reason for hiding this comment

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

Suggested change
LAST_ZONE = 'Autre'.freeze
OTHER_ZONE = 'Autre'

(pas besoin du frozen string avec le magic frozen_string_literal: true en ligne 1)

Copy link
Author

Choose a reason for hiding this comment

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

Oui c'est une partie que je voulais voir avec vous, car effectivement nous avons des zones différentes entre les différents forks, l'intention était de laisser la possibilité de choisir le nom de la zone "Autre" qui se placerait à la fin.

J'ai proposé ça mais s'il y a une idée plus générique qui peut convenir à tous clairement let's go !

Comment on lines 98 to 99
.partition { |zone| zone.label == Zone::LAST_ZONE }
.then { |last_zone, other_zones| other_zones + last_zone }
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
.partition { |zone| zone.label == Zone::LAST_ZONE }
.then { |last_zone, other_zones| other_zones + last_zone }
.partition { |zone| zone.label == Zone::OTHER_ZONE }
.then { |other_zone, zones| zones + other_zone }

it 'assigns @zones with the correct order' do
subject
assigned_labels = assigns(:zones).map(&:label)
expect(assigned_labels).to eq(['Zone 1', 'Zone 2', 'Autre'])
Copy link
Member

Choose a reason for hiding this comment

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

j'ai l'impression que ce test comprends les tests 508 et 514 non ?

Comment on lines 23 to 32
zones = Zone.includes(:labels).where.not(id: without_zones).to_a

zone_labels = zones.each_with_object({}) do |zone, labels|
label = zone.labels.filter { |l| l.designated_on < date }.max_by(&:designated_on)
labels[zone.id] = label&.name || ''
end

available_zones = zones.filter { |zone| zone_labels[zone.id] != 'Non attribué' }

available_zones.map do |zone|
Copy link
Member

Choose a reason for hiding this comment

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

Alors, je l'ai pas fait tournée mais voici ma proposition :

d'abords on peut passer label_at(date) en privée.
ensuite, on peut la redéfinir pour qu'elle ne fasse plus d'AR

def label_at(date)
  labels
    .sort { _1.designated_on }.reverse
    .filter { _1.designated_on < date}.first&.name || labels.last&.name
end

puis simplifier ce code

Suggested change
zones = Zone.includes(:labels).where.not(id: without_zones).to_a
zone_labels = zones.each_with_object({}) do |zone, labels|
label = zone.labels.filter { |l| l.designated_on < date }.max_by(&:designated_on)
labels[zone.id] = label&.name || ''
end
available_zones = zones.filter { |zone| zone_labels[zone.id] != 'Non attribué' }
available_zones.map do |zone|
Zone.includes(:labels).where.not(id: without_zones)
.filter { _1available_at?(date) }
.map do |zone|

WDYT ?

Copy link
Author

Choose a reason for hiding this comment

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

Bien vu pour éviter les allers retours ! Du coup j'ai regardé plus attentivement avec rack profiler !

Pour éviter les aller retours j'ai trouvé ceci, t'en pense quoi ? On se passe du label_at totalement du coup.

On passe de 58 requetes sql à 6 !

  def self.available_at(date, without_zones = [])
    # Charger toutes les zones qui ne sont pas dans without_zones
    zones = Zone.where.not(id: without_zones).order(:acronym).to_a

    # Charger tous les labels pertinents (ceux désignés avant la date donnée) en une seule requête
    labels_by_zone = ZoneLabel
      .where('designated_on <= ?', date)
      .where(zone_id: zones.map(&:id))
      .order(designated_on: :desc)
      .group_by(&:zone_id)

    # Préparer la liste des zones avec leurs labels
    zones.map do |zone|
      label = labels_by_zone[zone.id]&.first&.name
      next if label.nil? || label == 'Non attribué' # Exclure les zones sans label ou avec "Non attribué"
      LabelModel.new(id: zone.id, label: label)
    end.compact
  end

Ce qui nous permet de ne pas toucher à la méthode "label_at". Car là en l'état c'est une requete sql donc c'est pas mal.

enchainer les manip ça me parait un peu complexe non ?
faire un .sort -> O(n log n)
puis un .reverse -> O(n)
puis un .filter -> O(n)
ça nous fait enchainer du lourd niveau ressource je pense.

Une petite requête SQL avec le where et hop ! Qu'en penses tu ?

Copy link
Member

@LeSim LeSim left a comment

Choose a reason for hiding this comment

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

plusieurs choses:

  • le code me parait bon, j'approuve
  • les arguments de perfs m’embête un peu ici : c'est une page relativement peu usité et le nb de query était acceptable. Je pense que ce qui prime ici est la lisibilité du code. Si tu veux du challenge, regarde ici : https://oss.skylight.io/app/applications/zAvWTaqO0mu1/recent/6h/endpoints
  • des 0(n) pour des centaines d'entrées, c'est non mesurable.

finalement, on a codecov qui rale un peu au niveau de la couverture de test. t'es d'accord avec moi qu'il hallucine ?

let(:administrateur) { administrateurs(:default_admin) }
let(:procedure) { create(:procedure) }
let(:populate_zones_task) { Rake::Task['after_party:populate_zones_with_tchap_hs'] }
require 'rails_helper'
Copy link
Member

Choose a reason for hiding this comment

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

je crois qu'on load systématiquement le rails_helper

Suggested change
require 'rails_helper'

def current_label
labels.where.not(name: 'Non attribué').first.name
end

def label_at(date)
label = labels.where('designated_on < ?', date)&.first || labels.last
label.name
labels.where('designated_on < ?', date).first&.name || labels.last&.name
end

def available_at?(date)
Copy link
Member

Choose a reason for hiding this comment

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

pourrais tu supprimer cette méthode qui n'est pas utilisé dans un autre commit ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants