@@ -785,6 +785,9 @@ ERL_NIF_TERM nif_poll_events(ErlNifEnv *env, int argc,
785785 return enif_make_tuple2 (env , ATOM_OK , enif_make_int (env , num_events ));
786786}
787787
788+ /* Forward declaration for hash set clear function (defined with other hash functions) */
789+ static inline void pending_hash_clear (erlang_event_loop_t * loop );
790+
788791/**
789792 * get_pending(LoopRef) -> [{CallbackId, Type}]
790793 *
@@ -800,10 +803,28 @@ ERL_NIF_TERM nif_get_pending(ErlNifEnv *env, int argc,
800803 return enif_make_list (env , 0 );
801804 }
802805
806+ /*
807+ * Phase 1: Detach pending list under lock (fast - just pointer swap)
808+ * This minimizes lock contention by doing minimal work under the mutex.
809+ */
803810 pthread_mutex_lock (& loop -> mutex );
804811
812+ pending_event_t * snapshot_head = loop -> pending_head ;
813+ loop -> pending_head = NULL ;
814+ loop -> pending_tail = NULL ;
815+ atomic_store (& loop -> pending_count , 0 );
816+
817+ /* Clear the hash set since we're consuming all pending events */
818+ pending_hash_clear (loop );
819+
820+ pthread_mutex_unlock (& loop -> mutex );
821+
822+ /*
823+ * Phase 2: Build Erlang list outside lock (no contention)
824+ * Term creation and memory operations happen without holding the mutex.
825+ */
805826 ERL_NIF_TERM list = enif_make_list (env , 0 );
806- pending_event_t * current = loop -> pending_head ;
827+ pending_event_t * current = snapshot_head ;
807828
808829 while (current != NULL ) {
809830 ERL_NIF_TERM type_atom ;
@@ -833,12 +854,6 @@ ERL_NIF_TERM nif_get_pending(ErlNifEnv *env, int argc,
833854 current = next ;
834855 }
835856
836- loop -> pending_head = NULL ;
837- loop -> pending_tail = NULL ;
838- atomic_store (& loop -> pending_count , 0 );
839-
840- pthread_mutex_unlock (& loop -> mutex );
841-
842857 /* Reverse the list to maintain order */
843858 ERL_NIF_TERM reversed = enif_make_list (env , 0 );
844859 ERL_NIF_TERM head ;
@@ -1033,19 +1048,114 @@ static inline void return_pending_event(erlang_event_loop_t *loop,
10331048 }
10341049}
10351050
1051+ /* ============================================================================
1052+ * Pending Event Hash Set (O(1) duplicate detection)
1053+ *
1054+ * Uses open addressing with linear probing. Key is (callback_id, type)
1055+ * combined into a single uint64_t.
1056+ * ============================================================================ */
1057+
1058+ /**
1059+ * @brief Compute hash key from callback_id and event type
1060+ */
1061+ static inline uint64_t pending_hash_key (uint64_t callback_id , event_type_t type ) {
1062+ /* Combine callback_id and type into a single key */
1063+ return (callback_id << 2 ) | (uint64_t )type ;
1064+ }
1065+
1066+ /**
1067+ * @brief Compute hash bucket index
1068+ */
1069+ static inline uint32_t pending_hash_index (uint64_t key ) {
1070+ /* Simple hash: XOR fold and modulo */
1071+ return (uint32_t )((key ^ (key >> 32 )) % PENDING_HASH_SIZE );
1072+ }
1073+
1074+ /**
1075+ * @brief Check if a (callback_id, type) pair exists in the hash set
1076+ *
1077+ * @param loop Event loop containing the hash set
1078+ * @param callback_id Callback ID to check
1079+ * @param type Event type to check
1080+ * @return true if exists, false otherwise
1081+ */
1082+ static inline bool pending_hash_contains (erlang_event_loop_t * loop ,
1083+ uint64_t callback_id , event_type_t type ) {
1084+ if (loop -> pending_hash_count == 0 ) {
1085+ return false;
1086+ }
1087+
1088+ uint64_t key = pending_hash_key (callback_id , type );
1089+ uint32_t idx = pending_hash_index (key );
1090+
1091+ /* Linear probing */
1092+ for (int i = 0 ; i < PENDING_HASH_SIZE ; i ++ ) {
1093+ uint32_t probe = (idx + i ) % PENDING_HASH_SIZE ;
1094+ if (!loop -> pending_hash_occupied [probe ]) {
1095+ return false; /* Empty slot means key not present */
1096+ }
1097+ if (loop -> pending_hash_keys [probe ] == key ) {
1098+ return true;
1099+ }
1100+ }
1101+ return false; /* Table full, key not found */
1102+ }
1103+
1104+ /**
1105+ * @brief Insert a (callback_id, type) pair into the hash set
1106+ *
1107+ * @param loop Event loop containing the hash set
1108+ * @param callback_id Callback ID to insert
1109+ * @param type Event type to insert
1110+ * @return true if inserted, false if already exists or table full
1111+ */
1112+ static inline bool pending_hash_insert (erlang_event_loop_t * loop ,
1113+ uint64_t callback_id , event_type_t type ) {
1114+ /* Don't insert if table is too full (load factor > 0.75) */
1115+ if (loop -> pending_hash_count >= (PENDING_HASH_SIZE * 3 ) / 4 ) {
1116+ return false;
1117+ }
1118+
1119+ uint64_t key = pending_hash_key (callback_id , type );
1120+ uint32_t idx = pending_hash_index (key );
1121+
1122+ /* Linear probing */
1123+ for (int i = 0 ; i < PENDING_HASH_SIZE ; i ++ ) {
1124+ uint32_t probe = (idx + i ) % PENDING_HASH_SIZE ;
1125+ if (!loop -> pending_hash_occupied [probe ]) {
1126+ loop -> pending_hash_keys [probe ] = key ;
1127+ loop -> pending_hash_occupied [probe ] = true;
1128+ loop -> pending_hash_count ++ ;
1129+ return true;
1130+ }
1131+ if (loop -> pending_hash_keys [probe ] == key ) {
1132+ return false; /* Already exists */
1133+ }
1134+ }
1135+ return false; /* Table full */
1136+ }
1137+
1138+ /**
1139+ * @brief Clear the pending hash set
1140+ *
1141+ * @param loop Event loop containing the hash set
1142+ */
1143+ static inline void pending_hash_clear (erlang_event_loop_t * loop ) {
1144+ if (loop -> pending_hash_count > 0 ) {
1145+ memset (loop -> pending_hash_occupied , 0 , sizeof (loop -> pending_hash_occupied ));
1146+ loop -> pending_hash_count = 0 ;
1147+ }
1148+ }
1149+
10361150void event_loop_add_pending (erlang_event_loop_t * loop , event_type_t type ,
10371151 uint64_t callback_id , int fd ) {
10381152 pthread_mutex_lock (& loop -> mutex );
10391153
1040- /* Check for duplicate - don't add if same callback_id already pending */
1041- pending_event_t * current = loop -> pending_head ;
1042- while (current != NULL ) {
1043- if (current -> callback_id == callback_id && current -> type == type ) {
1044- /* Already have this event pending, skip */
1045- pthread_mutex_unlock (& loop -> mutex );
1046- return ;
1047- }
1048- current = current -> next ;
1154+ /* O(1) duplicate check using hash set */
1155+ if (pending_hash_contains (loop , callback_id , type )) {
1156+ /* Already have this event pending, skip */
1157+ pthread_mutex_unlock (& loop -> mutex );
1158+ return ;
10491159 }
10501160
10511161 /* Get event from freelist or allocate new (Phase 7 optimization) */
@@ -1068,6 +1178,9 @@ void event_loop_add_pending(erlang_event_loop_t *loop, event_type_t type,
10681178 loop -> pending_tail = event ;
10691179 }
10701180
1181+ /* Add to hash set for future O(1) duplicate checks */
1182+ pending_hash_insert (loop , callback_id , type );
1183+
10711184 atomic_fetch_add (& loop -> pending_count , 1 );
10721185 pthread_cond_signal (& loop -> event_cond );
10731186
@@ -1089,6 +1202,9 @@ void event_loop_clear_pending(erlang_event_loop_t *loop) {
10891202 loop -> pending_tail = NULL ;
10901203 atomic_store (& loop -> pending_count , 0 );
10911204
1205+ /* Clear the hash set since all pending events are cleared */
1206+ pending_hash_clear (loop );
1207+
10921208 pthread_mutex_unlock (& loop -> mutex );
10931209}
10941210
@@ -2084,6 +2200,9 @@ static PyObject *py_get_pending(PyObject *self, PyObject *args) {
20842200 loop -> pending_tail = NULL ;
20852201 atomic_store (& loop -> pending_count , 0 );
20862202
2203+ /* Clear the hash set since we're consuming all pending events */
2204+ pending_hash_clear (loop );
2205+
20872206 pthread_mutex_unlock (& loop -> mutex );
20882207
20892208 return list ;
@@ -2455,6 +2574,7 @@ static PyObject *py_run_once(PyObject *self, PyObject *args) {
24552574 loop -> pending_head = NULL ;
24562575 loop -> pending_tail = NULL ;
24572576 atomic_store (& loop -> pending_count , 0 );
2577+ pending_hash_clear (loop );
24582578 pthread_mutex_unlock (& loop -> mutex );
24592579 return NULL ;
24602580 }
@@ -2477,6 +2597,9 @@ static PyObject *py_run_once(PyObject *self, PyObject *args) {
24772597 loop -> pending_tail = NULL ;
24782598 atomic_store (& loop -> pending_count , 0 );
24792599
2600+ /* Clear the hash set since we're consuming all pending events */
2601+ pending_hash_clear (loop );
2602+
24802603 pthread_mutex_unlock (& loop -> mutex );
24812604
24822605 return list ;
0 commit comments