Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e21f4f1dfe | ||
|
|
e419cd5a07 |
@@ -180,7 +180,9 @@ class ConversationFinder
|
||||
|
||||
def conversations_base_query
|
||||
@conversations.includes(
|
||||
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox
|
||||
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox,
|
||||
{ cached_last_message: { attachments: { file_attachment: [:blob] } } },
|
||||
:cached_last_non_activity_message
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
class Migration::BackfillConversationCachedMessageIdsJob < ApplicationJob
|
||||
queue_as :low
|
||||
|
||||
def perform
|
||||
Conversation.find_in_batches(batch_size: 1000) do |conversations|
|
||||
conversations.each do |conversation|
|
||||
updates = {}
|
||||
|
||||
last_message = conversation.messages.reorder(created_at: :desc).first
|
||||
updates[:last_message_id] = last_message&.id
|
||||
|
||||
last_incoming = conversation.messages.incoming.reorder(created_at: :desc).first
|
||||
updates[:last_incoming_message_id] = last_incoming&.id
|
||||
|
||||
last_non_activity = conversation.messages.where.not(message_type: :activity).reorder(created_at: :desc).first
|
||||
updates[:last_non_activity_message_id] = last_non_activity&.id
|
||||
|
||||
conversation.update_columns(updates)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -105,6 +105,9 @@ class Conversation < ApplicationRecord
|
||||
belongs_to :contact_inbox
|
||||
belongs_to :team, optional: true
|
||||
belongs_to :campaign, optional: true
|
||||
belongs_to :cached_last_message, class_name: 'Message', optional: true, foreign_key: 'last_message_id'
|
||||
belongs_to :cached_last_incoming_message, class_name: 'Message', optional: true, foreign_key: 'last_incoming_message_id'
|
||||
belongs_to :cached_last_non_activity_message, class_name: 'Message', optional: true, foreign_key: 'last_non_activity_message_id'
|
||||
|
||||
has_many :mentions, dependent: :destroy_async
|
||||
has_many :messages, dependent: :destroy_async, autosave: true
|
||||
|
||||
+11
-1
@@ -114,7 +114,7 @@ class Message < ApplicationRecord
|
||||
|
||||
scope :created_since, ->(datetime) { where('created_at > ?', datetime) }
|
||||
scope :chat, -> { where.not(message_type: :activity).where(private: false) }
|
||||
scope :non_activity_messages, -> { where.not(message_type: :activity).reorder('id desc') }
|
||||
scope :non_activity_messages, -> { where.not(message_type: :activity).reorder('created_at desc') }
|
||||
scope :today, -> { where("date_trunc('day', created_at) = ?", Date.current) }
|
||||
scope :voice_calls, -> { where(content_type: :voice_call) }
|
||||
|
||||
@@ -133,6 +133,7 @@ class Message < ApplicationRecord
|
||||
has_many :notifications, as: :primary_actor, dependent: :destroy_async
|
||||
|
||||
after_create_commit :execute_after_create_commit_callbacks
|
||||
after_create_commit :update_conversation_cached_message_ids
|
||||
|
||||
after_update_commit :dispatch_update_event
|
||||
after_commit :reindex_for_search, if: :should_index?, on: [:create, :update]
|
||||
@@ -422,6 +423,15 @@ class Message < ApplicationRecord
|
||||
def reindex_for_search
|
||||
reindex(mode: :async)
|
||||
end
|
||||
|
||||
def update_conversation_cached_message_ids
|
||||
updates = { last_message_id: id }
|
||||
updates[:last_incoming_message_id] = id if incoming?
|
||||
updates[:last_non_activity_message_id] = id unless activity?
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
conversation.update_columns(updates)
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
end
|
||||
end
|
||||
|
||||
Message.prepend_mod_with('Message')
|
||||
|
||||
@@ -27,13 +27,10 @@ json.meta do
|
||||
end
|
||||
|
||||
json.id conversation.display_id
|
||||
if conversation.messages.where(account_id: conversation.account_id).last.blank?
|
||||
if conversation.cached_last_message.blank?
|
||||
json.messages []
|
||||
else
|
||||
json.messages [
|
||||
conversation.messages.where(account_id: conversation.account_id)
|
||||
.includes([{ attachments: [{ file_attachment: [:blob] }] }]).last.try(:push_event_data)
|
||||
]
|
||||
json.messages [conversation.cached_last_message.push_event_data]
|
||||
end
|
||||
|
||||
json.account_id conversation.account_id
|
||||
@@ -54,7 +51,7 @@ json.updated_at conversation.updated_at.to_f
|
||||
json.timestamp conversation.last_activity_at.to_i
|
||||
json.first_reply_created_at conversation.first_reply_created_at.to_i
|
||||
json.unread_count conversation.unread_incoming_messages.count
|
||||
json.last_non_activity_message conversation.messages.where(account_id: conversation.account_id).non_activity_messages.first.try(:push_event_data)
|
||||
json.last_non_activity_message conversation.cached_last_non_activity_message&.push_event_data
|
||||
json.last_activity_at conversation.last_activity_at.to_i
|
||||
json.priority conversation.priority
|
||||
json.waiting_since conversation.waiting_since.to_i.to_i
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
class AddCachedMessageIdsToConversations < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
add_column :conversations, :last_message_id, :bigint
|
||||
add_column :conversations, :last_incoming_message_id, :bigint
|
||||
add_column :conversations, :last_non_activity_message_id, :bigint
|
||||
|
||||
add_index :conversations, :last_message_id
|
||||
add_index :conversations, :last_incoming_message_id
|
||||
add_index :conversations, :last_non_activity_message_id
|
||||
end
|
||||
end
|
||||
+54
-33
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.1].define(version: 2026_01_20_121402) do
|
||||
ActiveRecord::Schema[7.1].define(version: 2026_01_28_194944) do
|
||||
# These extensions should be enabled to support this database
|
||||
enable_extension "pg_stat_statements"
|
||||
enable_extension "pg_trgm"
|
||||
@@ -71,6 +71,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_01_20_121402) do
|
||||
t.jsonb "limits", default: {}
|
||||
t.jsonb "custom_attributes", default: {}
|
||||
t.integer "status", default: 0
|
||||
t.integer "contactable_contacts_count", default: 0
|
||||
t.jsonb "internal_attributes", default: {}, null: false
|
||||
t.jsonb "settings", default: {}
|
||||
t.index ["status"], name: "index_accounts_on_status"
|
||||
@@ -589,13 +590,13 @@ ActiveRecord::Schema[7.1].define(version: 2026_01_20_121402) do
|
||||
t.bigint "account_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "contacts_count"
|
||||
t.integer "contacts_count", default: 0, null: false
|
||||
t.index ["account_id", "domain"], name: "index_companies_on_account_and_domain", unique: true, where: "(domain IS NOT NULL)"
|
||||
t.index ["account_id"], name: "index_companies_on_account_id"
|
||||
t.index ["name", "account_id"], name: "index_companies_on_name_and_account_id"
|
||||
end
|
||||
|
||||
create_table "contact_inboxes", force: :cascade do |t|
|
||||
create_table "contact_inboxes", id: :bigint, default: -> { "nextval('contact_inboxes2_id_seq'::regclass)" }, force: :cascade do |t|
|
||||
t.bigint "contact_id"
|
||||
t.bigint "inbox_id"
|
||||
t.text "source_id", null: false
|
||||
@@ -603,14 +604,14 @@ ActiveRecord::Schema[7.1].define(version: 2026_01_20_121402) do
|
||||
t.datetime "updated_at", null: false
|
||||
t.boolean "hmac_verified", default: false
|
||||
t.string "pubsub_token"
|
||||
t.index ["contact_id"], name: "index_contact_inboxes_on_contact_id"
|
||||
t.index ["inbox_id", "source_id"], name: "index_contact_inboxes_on_inbox_id_and_source_id", unique: true
|
||||
t.index ["inbox_id"], name: "index_contact_inboxes_on_inbox_id"
|
||||
t.index ["pubsub_token"], name: "index_contact_inboxes_on_pubsub_token", unique: true
|
||||
t.index ["source_id"], name: "index_contact_inboxes_on_source_id"
|
||||
t.index ["contact_id"], name: "contact_inboxes2_contact_id_idx"
|
||||
t.index ["inbox_id", "source_id"], name: "contact_inboxes2_inbox_id_source_id_idx", unique: true
|
||||
t.index ["inbox_id"], name: "contact_inboxes2_inbox_id_idx"
|
||||
t.index ["pubsub_token"], name: "contact_inboxes2_pubsub_token_idx", unique: true
|
||||
t.index ["source_id"], name: "contact_inboxes2_source_id_idx"
|
||||
end
|
||||
|
||||
create_table "contacts", id: :serial, force: :cascade do |t|
|
||||
create_table "contacts", id: :integer, default: -> { "nextval('contacts2_id_seq'::regclass)" }, force: :cascade do |t|
|
||||
t.string "name", default: ""
|
||||
t.string "email"
|
||||
t.string "phone_number"
|
||||
@@ -621,25 +622,27 @@ ActiveRecord::Schema[7.1].define(version: 2026_01_20_121402) do
|
||||
t.string "identifier"
|
||||
t.jsonb "custom_attributes", default: {}
|
||||
t.datetime "last_activity_at", precision: nil
|
||||
t.integer "contact_type", default: 0
|
||||
t.boolean "resolved", default: false, null: false
|
||||
t.integer "contact_type", default: 0, null: false
|
||||
t.string "middle_name", default: ""
|
||||
t.string "last_name", default: ""
|
||||
t.string "location", default: ""
|
||||
t.string "country_code", default: ""
|
||||
t.boolean "blocked", default: false, null: false
|
||||
t.bigint "company_id"
|
||||
t.index "lower((email)::text), account_id", name: "index_contacts_on_lower_email_account_id"
|
||||
t.index "lower((email)::text), account_id", name: "contacts2_lower_account_id_idx"
|
||||
t.index ["account_id", "contact_type"], name: "index_contacts_on_account_id_and_contact_type"
|
||||
t.index ["account_id", "email", "phone_number", "identifier"], name: "index_contacts_on_nonempty_fields", where: "(((email)::text <> ''::text) OR ((phone_number)::text <> ''::text) OR ((identifier)::text <> ''::text))"
|
||||
t.index ["account_id", "email", "phone_number", "identifier"], name: "contacts2_account_id_email_phone_number_identifier_idx", where: "(((email)::text <> ''::text) OR ((phone_number)::text <> ''::text) OR ((identifier)::text <> ''::text))"
|
||||
t.index ["account_id", "last_activity_at"], name: "index_contacts_on_account_id_and_last_activity_at", order: { last_activity_at: "DESC NULLS LAST" }
|
||||
t.index ["account_id"], name: "index_contacts_on_account_id"
|
||||
t.index ["account_id"], name: "index_resolved_contact_account_id", where: "(((email)::text <> ''::text) OR ((phone_number)::text <> ''::text) OR ((identifier)::text <> ''::text))"
|
||||
t.index ["account_id", "resolved"], name: "index_contacts_on_account_id_and_resolved"
|
||||
t.index ["account_id"], name: "contacts2_account_id_idx"
|
||||
t.index ["account_id"], name: "contacts2_account_id_idx1", where: "(((email)::text <> ''::text) OR ((phone_number)::text <> ''::text) OR ((identifier)::text <> ''::text))"
|
||||
t.index ["blocked"], name: "index_contacts_on_blocked"
|
||||
t.index ["company_id"], name: "index_contacts_on_company_id"
|
||||
t.index ["email", "account_id"], name: "uniq_email_per_account_contact", unique: true
|
||||
t.index ["identifier", "account_id"], name: "uniq_identifier_per_account_contact", unique: true
|
||||
t.index ["name", "email", "phone_number", "identifier"], name: "index_contacts_on_name_email_phone_number_identifier", opclass: :gin_trgm_ops, using: :gin
|
||||
t.index ["phone_number", "account_id"], name: "index_contacts_on_phone_number_and_account_id"
|
||||
t.index ["email", "account_id"], name: "contacts2_email_account_id_idx", unique: true
|
||||
t.index ["identifier", "account_id"], name: "contacts2_identifier_account_id_idx", unique: true
|
||||
t.index ["name", "email", "phone_number", "identifier"], name: "contacts2_name_email_phone_number_identifier_idx", opclass: :gin_trgm_ops, using: :gin
|
||||
t.index ["phone_number", "account_id"], name: "contacts2_phone_number_account_id_idx"
|
||||
end
|
||||
|
||||
create_table "conversation_participants", force: :cascade do |t|
|
||||
@@ -667,7 +670,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_01_20_121402) do
|
||||
t.datetime "agent_last_seen_at", precision: nil
|
||||
t.jsonb "additional_attributes", default: {}
|
||||
t.bigint "contact_inbox_id"
|
||||
t.uuid "uuid", default: -> { "gen_random_uuid()" }, null: false
|
||||
t.uuid "uuid", default: -> { "public.gen_random_uuid()" }, null: false
|
||||
t.string "identifier"
|
||||
t.datetime "last_activity_at", precision: nil, default: -> { "CURRENT_TIMESTAMP" }, null: false
|
||||
t.bigint "team_id"
|
||||
@@ -681,6 +684,9 @@ ActiveRecord::Schema[7.1].define(version: 2026_01_20_121402) do
|
||||
t.datetime "waiting_since"
|
||||
t.text "cached_label_list"
|
||||
t.bigint "assignee_agent_bot_id"
|
||||
t.bigint "last_message_id"
|
||||
t.bigint "last_incoming_message_id"
|
||||
t.bigint "last_non_activity_message_id"
|
||||
t.index ["account_id", "display_id"], name: "index_conversations_on_account_id_and_display_id", unique: true
|
||||
t.index ["account_id", "id"], name: "index_conversations_on_id_and_account_id"
|
||||
t.index ["account_id", "inbox_id", "status", "assignee_id"], name: "conv_acid_inbid_stat_asgnid_idx"
|
||||
@@ -692,6 +698,9 @@ ActiveRecord::Schema[7.1].define(version: 2026_01_20_121402) do
|
||||
t.index ["first_reply_created_at"], name: "index_conversations_on_first_reply_created_at"
|
||||
t.index ["identifier", "account_id"], name: "index_conversations_on_identifier_and_account_id"
|
||||
t.index ["inbox_id"], name: "index_conversations_on_inbox_id"
|
||||
t.index ["last_incoming_message_id"], name: "index_conversations_on_last_incoming_message_id"
|
||||
t.index ["last_message_id"], name: "index_conversations_on_last_message_id"
|
||||
t.index ["last_non_activity_message_id"], name: "index_conversations_on_last_non_activity_message_id"
|
||||
t.index ["priority"], name: "index_conversations_on_priority"
|
||||
t.index ["status", "account_id"], name: "index_conversations_on_status_and_account_id"
|
||||
t.index ["status", "priority"], name: "index_conversations_on_status_and_priority"
|
||||
@@ -758,7 +767,6 @@ ActiveRecord::Schema[7.1].define(version: 2026_01_20_121402) do
|
||||
t.string "regex_pattern"
|
||||
t.string "regex_cue"
|
||||
t.index ["account_id"], name: "index_custom_attribute_definitions_on_account_id"
|
||||
t.index ["attribute_key", "attribute_model", "account_id"], name: "attribute_key_model_index", unique: true
|
||||
end
|
||||
|
||||
create_table "custom_filters", force: :cascade do |t|
|
||||
@@ -941,7 +949,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_01_20_121402) do
|
||||
t.integer "visibility", default: 0
|
||||
t.bigint "created_by_id"
|
||||
t.bigint "updated_by_id"
|
||||
t.jsonb "actions", default: {}, null: false
|
||||
t.jsonb "actions", default: "{}", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["account_id"], name: "index_macros_on_account_id"
|
||||
@@ -960,7 +968,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_01_20_121402) do
|
||||
t.index ["user_id"], name: "index_mentions_on_user_id"
|
||||
end
|
||||
|
||||
create_table "messages", id: :serial, force: :cascade do |t|
|
||||
create_table "messages", id: :integer, default: -> { "nextval('messages2_id_seq'::regclass)" }, force: :cascade do |t|
|
||||
t.text "content"
|
||||
t.integer "account_id", null: false
|
||||
t.integer "inbox_id", null: false
|
||||
@@ -979,18 +987,18 @@ ActiveRecord::Schema[7.1].define(version: 2026_01_20_121402) do
|
||||
t.jsonb "additional_attributes", default: {}
|
||||
t.text "processed_message_content"
|
||||
t.jsonb "sentiment", default: {}
|
||||
t.index "((additional_attributes -> 'campaign_id'::text))", name: "index_messages_on_additional_attributes_campaign_id", using: :gin
|
||||
t.index "((additional_attributes -> 'campaign_id'::text))", name: "messages2_expr_idx", using: :gin
|
||||
t.index ["account_id", "content_type", "created_at"], name: "idx_messages_account_content_created"
|
||||
t.index ["account_id", "created_at", "message_type"], name: "index_messages_on_account_created_type"
|
||||
t.index ["account_id", "inbox_id"], name: "index_messages_on_account_id_and_inbox_id"
|
||||
t.index ["account_id"], name: "index_messages_on_account_id"
|
||||
t.index ["content"], name: "index_messages_on_content", opclass: :gin_trgm_ops, using: :gin
|
||||
t.index ["conversation_id", "account_id", "message_type", "created_at"], name: "index_messages_on_conversation_account_type_created"
|
||||
t.index ["conversation_id"], name: "index_messages_on_conversation_id"
|
||||
t.index ["created_at"], name: "index_messages_on_created_at"
|
||||
t.index ["inbox_id"], name: "index_messages_on_inbox_id"
|
||||
t.index ["sender_type", "sender_id"], name: "index_messages_on_sender_type_and_sender_id"
|
||||
t.index ["source_id"], name: "index_messages_on_source_id"
|
||||
t.index ["account_id", "inbox_id"], name: "messages2_account_id_inbox_id_idx"
|
||||
t.index ["account_id"], name: "messages2_account_id_idx"
|
||||
t.index ["content"], name: "messages2_content_idx", opclass: :gin_trgm_ops, using: :gin
|
||||
t.index ["conversation_id", "account_id", "message_type", "created_at"], name: "messages2_conversation_id_account_id_message_type_created_a_idx"
|
||||
t.index ["conversation_id"], name: "messages2_conversation_id_idx"
|
||||
t.index ["created_at"], name: "messages2_created_at_idx"
|
||||
t.index ["inbox_id"], name: "messages2_inbox_id_idx"
|
||||
t.index ["sender_type", "sender_id"], name: "messages2_sender_type_sender_id_idx"
|
||||
t.index ["source_id"], name: "messages2_source_id_idx"
|
||||
end
|
||||
|
||||
create_table "notes", force: :cascade do |t|
|
||||
@@ -1048,6 +1056,19 @@ ActiveRecord::Schema[7.1].define(version: 2026_01_20_121402) do
|
||||
t.index ["user_id"], name: "index_notifications_on_user_id"
|
||||
end
|
||||
|
||||
create_table "pg_search_documents", force: :cascade do |t|
|
||||
t.text "content"
|
||||
t.bigint "conversation_id"
|
||||
t.bigint "account_id"
|
||||
t.string "searchable_type"
|
||||
t.bigint "searchable_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["account_id"], name: "index_pg_search_documents_on_account_id"
|
||||
t.index ["conversation_id"], name: "index_pg_search_documents_on_conversation_id"
|
||||
t.index ["searchable_type", "searchable_id"], name: "index_pg_search_documents_on_searchable"
|
||||
end
|
||||
|
||||
create_table "platform_app_permissibles", force: :cascade do |t|
|
||||
t.bigint "platform_app_id", null: false
|
||||
t.string "permissible_type", null: false
|
||||
@@ -1230,7 +1251,7 @@ ActiveRecord::Schema[7.1].define(version: 2026_01_20_121402) do
|
||||
t.text "message_signature"
|
||||
t.string "otp_secret"
|
||||
t.integer "consumed_timestep"
|
||||
t.boolean "otp_required_for_login", default: false
|
||||
t.boolean "otp_required_for_login", default: false, null: false
|
||||
t.text "otp_backup_codes"
|
||||
t.index ["email"], name: "index_users_on_email"
|
||||
t.index ["otp_required_for_login"], name: "index_users_on_otp_required_for_login"
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# Usage: load 'scripts/benchmark_campaigns.rb'; benchmark_campaigns('website_token')
|
||||
|
||||
def benchmark_campaigns(website_token)
|
||||
web_widget = Channel::WebWidget.find_by(website_token: website_token)
|
||||
raise "WebWidget not found for token #{website_token}" unless web_widget
|
||||
|
||||
inbox = web_widget.inbox
|
||||
account = inbox.account
|
||||
|
||||
puts "Inbox: #{inbox.id} | Account: #{account.id}"
|
||||
puts "Campaigns feature enabled: #{account.feature_enabled?('campaigns')}"
|
||||
|
||||
queries = []
|
||||
subscription = ActiveSupport::Notifications.subscribe('sql.active_record') do |_name, start, finish, _id, payload|
|
||||
binds = payload[:type_casted_binds] || payload[:binds]&.map(&:value_for_database) || []
|
||||
queries << { sql: payload[:sql], duration: (finish - start) * 1000, binds: binds }
|
||||
end
|
||||
|
||||
ActiveRecord::Base.uncached do
|
||||
start = Time.now
|
||||
@campaigns = inbox.campaigns
|
||||
.where(enabled: true, account_id: account.id)
|
||||
.includes(:sender).to_a
|
||||
@query_time = Time.now - start
|
||||
end
|
||||
|
||||
ActiveSupport::Notifications.unsubscribe(subscription)
|
||||
|
||||
puts "\n=== Timing ==="
|
||||
puts "Query: #{(@query_time * 1000).round(2)}ms"
|
||||
puts "Queries: #{queries.size}"
|
||||
puts "Campaigns found: #{@campaigns.size}"
|
||||
|
||||
puts "\n=== All Queries ==="
|
||||
queries.each_with_index do |q, i|
|
||||
puts "\n#{i + 1}. #{q[:duration].round(2)}ms"
|
||||
puts " #{q[:sql][0..200]}"
|
||||
puts " Binds: #{q[:binds].inspect[0..150]}" if q[:binds]&.any?
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
@@ -0,0 +1,72 @@
|
||||
# Usage: load 'scripts/benchmark_contacts.rb'; benchmark_contacts(account_id, user_id)
|
||||
# With action: benchmark_contacts(account_id, user_id, action: 'search', q: 'john')
|
||||
|
||||
def benchmark_contacts(account_id, user_id, action: 'index', **opts)
|
||||
account = Account.find(account_id)
|
||||
user = User.find(user_id)
|
||||
Current.account = account
|
||||
Current.user = user
|
||||
|
||||
puts "Account: #{account_id} | User: #{user_id} | Action: #{action}"
|
||||
|
||||
queries = []
|
||||
subscription = ActiveSupport::Notifications.subscribe('sql.active_record') do |_name, start, finish, _id, payload|
|
||||
binds = payload[:type_casted_binds] || payload[:binds]&.map(&:value_for_database) || []
|
||||
queries << { sql: payload[:sql], duration: (finish - start) * 1000, binds: binds }
|
||||
end
|
||||
|
||||
ActiveRecord::Base.uncached do
|
||||
start = Time.now
|
||||
|
||||
case action
|
||||
when 'index'
|
||||
contacts = account.contacts.resolved_contacts
|
||||
@contacts = contacts
|
||||
.includes(avatar_attachment: [:blob], contact_inboxes: { inbox: :channel })
|
||||
.page(opts[:page] || 1)
|
||||
.per(15).to_a
|
||||
@count = contacts.count
|
||||
|
||||
when 'search'
|
||||
q = opts[:q] || ''
|
||||
contacts = account.contacts.where(
|
||||
'name ILIKE :search OR email ILIKE :search OR phone_number ILIKE :search OR contacts.identifier LIKE :search',
|
||||
search: "%#{q.strip}%"
|
||||
)
|
||||
@contacts = contacts
|
||||
.includes(avatar_attachment: [:blob], contact_inboxes: { inbox: :channel })
|
||||
.page(opts[:page] || 1)
|
||||
.per(15).to_a
|
||||
@count = contacts.count
|
||||
|
||||
when 'filter'
|
||||
# Requires payload param - simplified version
|
||||
@contacts = account.contacts.page(1).per(15).to_a
|
||||
@count = account.contacts.count
|
||||
end
|
||||
|
||||
@query_time = Time.now - start
|
||||
end
|
||||
|
||||
ActiveSupport::Notifications.unsubscribe(subscription)
|
||||
|
||||
puts "\n=== Timing ==="
|
||||
puts "Query: #{(@query_time * 1000).round(2)}ms"
|
||||
puts "Queries: #{queries.size}"
|
||||
puts "Contacts found: #{@contacts&.size}"
|
||||
puts "Total count: #{@count}"
|
||||
|
||||
puts "\n=== Top 10 Slowest Queries ==="
|
||||
queries.select { |q| q[:duration] }.sort_by { |q| -q[:duration] }.first(10).each_with_index do |q, i|
|
||||
puts "\n#{i + 1}. #{q[:duration].round(2)}ms"
|
||||
puts " #{q[:sql][0..200]}"
|
||||
puts " Binds: #{q[:binds].inspect[0..150]}" if q[:binds]&.any?
|
||||
end
|
||||
|
||||
puts "\n=== All Queries (grouped by pattern) ==="
|
||||
queries.group_by { |q| q[:sql][0..100] }.sort_by { |_, qs| -qs.size }.each do |sql, qs|
|
||||
puts " #{qs.size}x #{qs.sum { |q| q[:duration] }.round(2)}ms - #{sql[0..80]}..."
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
@@ -0,0 +1,92 @@
|
||||
# Usage: load 'scripts/benchmark_conversations.rb'; benchmark_conversations(91698, 89557)
|
||||
# With options: benchmark_conversations(91698, 89557, assignee_type: 'all', status: 'open', team_id: 123)
|
||||
|
||||
def benchmark_conversations(account_id, user_id, assignee_type: 'me', status: 'open', team_id: nil)
|
||||
account = Account.find(account_id)
|
||||
user = User.find(user_id)
|
||||
Current.account = account
|
||||
Current.user = user
|
||||
params = { status: status, assignee_type: assignee_type, page: 1, sort_by: 'last_activity_at_desc' }
|
||||
params[:team_id] = team_id if team_id
|
||||
|
||||
queries = []
|
||||
subscription = ActiveSupport::Notifications.subscribe('sql.active_record') do |_name, start, finish, _id, payload|
|
||||
binds = payload[:type_casted_binds] || payload[:binds]&.map(&:value_for_database) || []
|
||||
bt = caller.select { |l| l.include?('chatwoot') }.first(5)
|
||||
queries << { sql: payload[:sql], duration: (finish - start) * 1000, binds: binds, backtrace: bt }
|
||||
end
|
||||
|
||||
# Disable ActiveRecord query cache
|
||||
ActiveRecord::Base.uncached do
|
||||
start = Time.now
|
||||
finder = ConversationFinder.new(user, params)
|
||||
result = finder.perform
|
||||
conversations = result[:conversations]
|
||||
@finder_time = Time.now - start
|
||||
@conversations = conversations
|
||||
|
||||
start = Time.now
|
||||
conversations.each do |conversation|
|
||||
ApplicationController.renderer.render(
|
||||
partial: 'api/v1/conversations/partials/conversation',
|
||||
locals: { conversation: conversation },
|
||||
formats: [:json]
|
||||
)
|
||||
end
|
||||
@render_time = Time.now - start
|
||||
end
|
||||
|
||||
ActiveSupport::Notifications.unsubscribe(subscription)
|
||||
finder_time = @finder_time
|
||||
render_time = @render_time
|
||||
conversations = @conversations
|
||||
|
||||
puts "\n=== Timing ==="
|
||||
puts "Finder: #{(finder_time * 1000).round(1)}ms"
|
||||
puts "Render: #{(render_time * 1000).round(1)}ms"
|
||||
puts "Total: #{((finder_time + render_time) * 1000).round(1)}ms"
|
||||
puts "Queries: #{queries.size}"
|
||||
|
||||
puts "\n=== Top 10 Slowest Queries ==="
|
||||
queries.select { |q| q[:duration] }.sort_by { |q| -q[:duration] }.first(10).each_with_index do |q, i|
|
||||
puts "\n#{i + 1}. #{q[:duration].round(2)}ms"
|
||||
puts " #{q[:sql][0..200]}"
|
||||
puts " Binds: #{q[:binds].inspect[0..200]}" if q[:binds]&.any?
|
||||
puts " Source: #{q[:backtrace]&.first}" if q[:backtrace]&.any?
|
||||
end
|
||||
|
||||
# Group queries by conversation_id
|
||||
conv_ids = conversations.map(&:id)
|
||||
conv_queries = Hash.new { |h, k| h[k] = [] }
|
||||
|
||||
queries.each do |q|
|
||||
next unless q[:binds]&.any?
|
||||
|
||||
conv_id = q[:binds].find { |b| conv_ids.include?(b) }
|
||||
conv_queries[conv_id] << q if conv_id
|
||||
end
|
||||
|
||||
puts "\n=== Queries Per Conversation ==="
|
||||
conv_queries.sort_by { |_, qs| -qs.sum { |q| q[:duration] } }.first(5).each do |conv_id, qs|
|
||||
total_ms = qs.sum { |q| q[:duration] }.round(2)
|
||||
puts "\nConversation #{conv_id}: #{qs.size} queries, #{total_ms}ms total"
|
||||
qs.group_by { |q| q[:sql][0..80] }.each do |sql, grouped|
|
||||
puts " #{grouped.size}x #{grouped.sum { |q| q[:duration] }.round(2)}ms - #{sql[0..60]}..."
|
||||
end
|
||||
end
|
||||
|
||||
puts "\n=== Summary ==="
|
||||
puts "Conversations: #{conversations.size}"
|
||||
puts "Avg queries/conversation: #{(conv_queries.values.sum(&:size).to_f / conversations.size).round(1)}"
|
||||
|
||||
tied_queries = conv_queries.values.flatten
|
||||
untied = queries - tied_queries
|
||||
puts "Queries not tied to conversation: #{untied.size}"
|
||||
|
||||
puts "\n=== Untied Queries (grouped by pattern) ==="
|
||||
untied.group_by { |q| q[:sql][0..100] }.sort_by { |_, qs| -qs.size }.first(15).each do |sql, qs|
|
||||
puts " #{qs.size}x #{qs.sum { |q| q[:duration] }.round(2)}ms - #{sql[0..80]}..."
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
Reference in New Issue
Block a user