Profiling.cpp
1 /* 2 * Copyright (c) 2013-2025, The PurpleI2P Project 3 * 4 * This file is part of Purple i2pd project and licensed under BSD3 5 * 6 * See full license text in LICENSE file at top of project tree 7 */ 8 9 #include <sys/stat.h> 10 #include <unordered_map> 11 #include <list> 12 #include <thread> 13 #include <iomanip> 14 #include <boost/property_tree/ptree.hpp> 15 #include <boost/property_tree/ini_parser.hpp> 16 #include "Base.h" 17 #include "FS.h" 18 #include "Log.h" 19 #include "Timestamp.h" 20 #include "NetDb.hpp" 21 #include "Profiling.h" 22 23 namespace i2p 24 { 25 namespace data 26 { 27 static i2p::fs::HashedStorage g_ProfilesStorage("peerProfiles", "p", "profile-", "txt"); 28 static std::unordered_map<i2p::data::IdentHash, std::shared_ptr<RouterProfile> > g_Profiles; 29 static std::mutex g_ProfilesMutex; 30 static std::list<std::pair<i2p::data::IdentHash, std::function<void (std::shared_ptr<RouterProfile>)> > > g_PostponedUpdates; 31 static std::mutex g_PostponedUpdatesMutex; 32 33 RouterProfile::RouterProfile (): 34 m_IsUpdated (false), m_LastDeclineTime (0), m_LastUnreachableTime (0), 35 m_LastUpdateTime (i2p::util::GetSecondsSinceEpoch ()), m_LastAccessTime (0), 36 m_LastPersistTime (0), m_NumTunnelsAgreed (0), m_NumTunnelsDeclined (0), 37 m_NumTunnelsNonReplied (0),m_NumTimesTaken (0), m_NumTimesRejected (0), 38 m_HasConnected (false), m_IsDuplicated (false) 39 { 40 } 41 42 void RouterProfile::UpdateTime () 43 { 44 m_LastUpdateTime = i2p::util::GetSecondsSinceEpoch (); 45 m_IsUpdated = true; 46 } 47 48 void RouterProfile::Save (const IdentHash& identHash) 49 { 50 // fill sections 51 boost::property_tree::ptree participation; 52 participation.put (PEER_PROFILE_PARTICIPATION_AGREED, m_NumTunnelsAgreed); 53 participation.put (PEER_PROFILE_PARTICIPATION_DECLINED, m_NumTunnelsDeclined); 54 participation.put (PEER_PROFILE_PARTICIPATION_NON_REPLIED, m_NumTunnelsNonReplied); 55 boost::property_tree::ptree usage; 56 usage.put (PEER_PROFILE_USAGE_TAKEN, m_NumTimesTaken); 57 usage.put (PEER_PROFILE_USAGE_REJECTED, m_NumTimesRejected); 58 usage.put (PEER_PROFILE_USAGE_CONNECTED, m_HasConnected); 59 if (m_IsDuplicated) 60 usage.put (PEER_PROFILE_USAGE_DUPLICATED, true); 61 // fill property tree 62 boost::property_tree::ptree pt; 63 pt.put (PEER_PROFILE_LAST_UPDATE_TIMESTAMP, m_LastUpdateTime); 64 if (m_LastUnreachableTime) 65 pt.put (PEER_PROFILE_LAST_UNREACHABLE_TIME, m_LastUnreachableTime); 66 pt.put_child (PEER_PROFILE_SECTION_PARTICIPATION, participation); 67 pt.put_child (PEER_PROFILE_SECTION_USAGE, usage); 68 69 // save to file 70 std::string ident = identHash.ToBase64 (); 71 std::string path = g_ProfilesStorage.Path(ident); 72 73 try { 74 boost::property_tree::write_ini (path, pt); 75 } catch (std::exception& ex) { 76 /* boost exception verbose enough */ 77 LogPrint (eLogError, "Profiling: ", ex.what ()); 78 } 79 } 80 81 void RouterProfile::Load (const IdentHash& identHash) 82 { 83 m_IsUpdated = false; 84 std::string ident = identHash.ToBase64 (); 85 std::string path = g_ProfilesStorage.Path(ident); 86 boost::property_tree::ptree pt; 87 88 if (!i2p::fs::Exists(path)) 89 { 90 LogPrint(eLogWarning, "Profiling: No profile yet for ", ident); 91 return; 92 } 93 94 try 95 { 96 boost::property_tree::read_ini (path, pt); 97 } catch (std::exception& ex) 98 { 99 /* boost exception verbose enough */ 100 LogPrint (eLogError, "Profiling: ", ex.what ()); 101 return; 102 } 103 104 try 105 { 106 auto ts = pt.get (PEER_PROFILE_LAST_UPDATE_TIMESTAMP, 0); 107 if (ts) 108 m_LastUpdateTime = ts; 109 else 110 { 111 // try old lastupdatetime 112 auto ut = pt.get (PEER_PROFILE_LAST_UPDATE_TIME, ""); 113 if (ut.length () > 0) 114 { 115 std::istringstream ss (ut); std::tm t; 116 ss >> std::get_time(&t, "%Y-%b-%d %H:%M:%S"); 117 if (!ss.fail()) 118 m_LastUpdateTime = mktime (&t); // t is local time 119 } 120 } 121 if (i2p::util::GetSecondsSinceEpoch () - m_LastUpdateTime < PEER_PROFILE_EXPIRATION_TIMEOUT) 122 { 123 m_LastUnreachableTime = pt.get (PEER_PROFILE_LAST_UNREACHABLE_TIME, 0); 124 try 125 { 126 // read participations 127 auto participations = pt.get_child (PEER_PROFILE_SECTION_PARTICIPATION); 128 m_NumTunnelsAgreed = participations.get (PEER_PROFILE_PARTICIPATION_AGREED, 0); 129 m_NumTunnelsDeclined = participations.get (PEER_PROFILE_PARTICIPATION_DECLINED, 0); 130 m_NumTunnelsNonReplied = participations.get (PEER_PROFILE_PARTICIPATION_NON_REPLIED, 0); 131 } 132 catch (boost::property_tree::ptree_bad_path& ex) 133 { 134 LogPrint (eLogWarning, "Profiling: Missing section ", PEER_PROFILE_SECTION_PARTICIPATION, " in profile for ", ident); 135 } 136 try 137 { 138 // read usage 139 auto usage = pt.get_child (PEER_PROFILE_SECTION_USAGE); 140 m_NumTimesTaken = usage.get (PEER_PROFILE_USAGE_TAKEN, 0); 141 m_NumTimesRejected = usage.get (PEER_PROFILE_USAGE_REJECTED, 0); 142 m_HasConnected = usage.get (PEER_PROFILE_USAGE_CONNECTED, false); 143 m_IsDuplicated = usage.get (PEER_PROFILE_USAGE_DUPLICATED, false); 144 } 145 catch (boost::property_tree::ptree_bad_path& ex) 146 { 147 LogPrint (eLogWarning, "Profiling: Missing section ", PEER_PROFILE_SECTION_USAGE, " in profile for ", ident); 148 } 149 } 150 else 151 *this = RouterProfile (); 152 } 153 catch (std::exception& ex) 154 { 155 LogPrint (eLogError, "Profiling: Can't read profile ", ident, " :", ex.what ()); 156 } 157 } 158 159 void RouterProfile::TunnelBuildResponse (uint8_t ret) 160 { 161 UpdateTime (); 162 if (ret > 0) 163 { 164 m_NumTunnelsDeclined++; 165 m_LastDeclineTime = i2p::util::GetSecondsSinceEpoch (); 166 } 167 else 168 { 169 m_NumTunnelsAgreed++; 170 m_LastDeclineTime = 0; 171 } 172 } 173 174 void RouterProfile::TunnelNonReplied () 175 { 176 m_NumTunnelsNonReplied++; 177 UpdateTime (); 178 if (m_NumTunnelsNonReplied > 2*m_NumTunnelsAgreed && m_NumTunnelsNonReplied > 3) 179 { 180 m_LastDeclineTime = i2p::util::GetSecondsSinceEpoch (); 181 } 182 } 183 184 void RouterProfile::Unreachable (bool unreachable) 185 { 186 m_LastUnreachableTime = unreachable ? i2p::util::GetSecondsSinceEpoch () : 0; 187 UpdateTime (); 188 } 189 190 void RouterProfile::Connected () 191 { 192 m_HasConnected = true; 193 UpdateTime (); 194 } 195 196 void RouterProfile::Duplicated () 197 { 198 m_IsDuplicated = true; 199 } 200 201 bool RouterProfile::IsLowPartcipationRate () const 202 { 203 return 4*m_NumTunnelsAgreed < m_NumTunnelsDeclined; // < 20% rate 204 } 205 206 bool RouterProfile::IsLowReplyRate () const 207 { 208 auto total = m_NumTunnelsAgreed + m_NumTunnelsDeclined; 209 return m_NumTunnelsNonReplied > 10*(total + 1); 210 } 211 212 bool RouterProfile::IsDeclinedRecently (uint64_t ts) 213 { 214 if (!m_LastDeclineTime) return false; 215 if (ts > m_LastDeclineTime + PEER_PROFILE_DECLINED_RECENTLY_INTERVAL || 216 ts + PEER_PROFILE_DECLINED_RECENTLY_INTERVAL < m_LastDeclineTime) 217 m_LastDeclineTime = 0; 218 return (bool)m_LastDeclineTime; 219 } 220 221 bool RouterProfile::IsBad () 222 { 223 if (IsUnreachable () || m_IsDuplicated) return true; 224 auto ts = i2p::util::GetSecondsSinceEpoch (); 225 if (ts > PEER_PROFILE_MAX_DECLINED_INTERVAL + m_LastDeclineTime) return false; 226 if (IsDeclinedRecently (ts)) return true; 227 auto isBad = IsAlwaysDeclining () || IsLowPartcipationRate () /*|| IsLowReplyRate ()*/; 228 if (isBad && m_NumTimesRejected > 10*(m_NumTimesTaken + 1)) 229 { 230 // reset profile 231 m_NumTunnelsAgreed = 0; 232 m_NumTunnelsDeclined = 0; 233 m_NumTunnelsNonReplied = 0; 234 isBad = false; 235 } 236 if (isBad) m_NumTimesRejected++; else m_NumTimesTaken++; 237 return isBad; 238 } 239 240 bool RouterProfile::IsUnreachable () 241 { 242 if (!m_LastUnreachableTime) return false; 243 auto ts = i2p::util::GetSecondsSinceEpoch (); 244 if (ts > m_LastUnreachableTime + PEER_PROFILE_UNREACHABLE_INTERVAL || 245 ts + PEER_PROFILE_UNREACHABLE_INTERVAL < m_LastUnreachableTime) 246 m_LastUnreachableTime = 0; 247 return (bool)m_LastUnreachableTime; 248 } 249 250 bool RouterProfile::IsUseful() const 251 { 252 return IsReal () || m_NumTunnelsNonReplied >= PEER_PROFILE_USEFUL_THRESHOLD; 253 } 254 255 std::shared_ptr<RouterProfile> GetRouterProfile (const IdentHash& identHash) 256 { 257 { 258 std::unique_lock<std::mutex> l(g_ProfilesMutex); 259 auto it = g_Profiles.find (identHash); 260 if (it != g_Profiles.end ()) 261 { 262 it->second->SetLastAccessTime (i2p::util::GetSecondsSinceEpoch ()); 263 return it->second; 264 } 265 } 266 auto profile = netdb.NewRouterProfile (); 267 profile->Load (identHash); // if possible 268 std::lock_guard<std::mutex> l(g_ProfilesMutex); 269 g_Profiles.emplace (identHash, profile); 270 return profile; 271 } 272 273 bool IsRouterBanned (const IdentHash& identHash) 274 { 275 std::lock_guard<std::mutex> l(g_ProfilesMutex); 276 auto it = g_Profiles.find (identHash); 277 if (it != g_Profiles.end ()) 278 return it->second->IsUnreachable (); 279 return false; 280 } 281 282 bool IsRouterDuplicated (const IdentHash& identHash) 283 { 284 std::lock_guard<std::mutex> l(g_ProfilesMutex); 285 auto it = g_Profiles.find (identHash); 286 if (it != g_Profiles.end ()) 287 return it->second->IsDuplicated (); 288 return false; 289 } 290 291 void InitProfilesStorage () 292 { 293 g_ProfilesStorage.SetPlace(i2p::fs::GetDataDir()); 294 g_ProfilesStorage.Init(i2p::data::GetBase64SubstitutionTable(), 64); 295 } 296 297 static void SaveProfilesToDisk (std::list<std::pair<i2p::data::IdentHash, std::shared_ptr<RouterProfile> > >&& profiles) 298 { 299 for (auto& it: profiles) 300 if (it.second) it.second->Save (it.first); 301 } 302 303 std::future<void> PersistProfiles () 304 { 305 auto ts = i2p::util::GetSecondsSinceEpoch (); 306 std::list<std::pair<i2p::data::IdentHash, std::shared_ptr<RouterProfile> > > tmp; 307 { 308 std::lock_guard<std::mutex> l(g_ProfilesMutex); 309 for (auto it = g_Profiles.begin (); it != g_Profiles.end ();) 310 { 311 if (it->second->IsUpdated () && ts > it->second->GetLastPersistTime () + PEER_PROFILE_PERSIST_INTERVAL) 312 { 313 tmp.push_back (*it); 314 it->second->SetLastPersistTime (ts); 315 it->second->SetUpdated (false); 316 } 317 if (!it->second->IsUpdated () && ts > std::max (it->second->GetLastUpdateTime (), it->second->GetLastAccessTime ()) + PEER_PROFILE_PERSIST_INTERVAL) 318 it = g_Profiles.erase (it); 319 else 320 it++; 321 } 322 } 323 if (!tmp.empty ()) 324 return std::async (std::launch::async, SaveProfilesToDisk, std::move (tmp)); 325 return std::future<void>(); 326 } 327 328 void SaveProfiles () 329 { 330 std::unordered_map<i2p::data::IdentHash, std::shared_ptr<RouterProfile> > tmp; 331 { 332 std::lock_guard<std::mutex> l(g_ProfilesMutex); 333 std::swap (tmp, g_Profiles); 334 } 335 auto ts = i2p::util::GetSecondsSinceEpoch (); 336 for (auto& it: tmp) 337 if (it.second->IsUseful() && (it.second->IsUpdated () || ts - it.second->GetLastUpdateTime () < PEER_PROFILE_EXPIRATION_TIMEOUT)) 338 it.second->Save (it.first); 339 } 340 341 static void DeleteFilesFromDisk () 342 { 343 std::vector<std::string> files; 344 g_ProfilesStorage.Traverse(files); 345 346 struct stat st; 347 std::time_t now = std::time(nullptr); 348 for (const auto& path: files) 349 { 350 if (stat(path.c_str(), &st) != 0) 351 { 352 LogPrint(eLogWarning, "Profiling: Can't stat(): ", path); 353 continue; 354 } 355 if (now - st.st_mtime >= PEER_PROFILE_EXPIRATION_TIMEOUT) 356 { 357 LogPrint(eLogDebug, "Profiling: Removing expired peer profile: ", path); 358 i2p::fs::Remove(path); 359 } 360 } 361 } 362 363 std::future<void> DeleteObsoleteProfiles () 364 { 365 { 366 auto ts = i2p::util::GetSecondsSinceEpoch (); 367 std::lock_guard<std::mutex> l(g_ProfilesMutex); 368 for (auto it = g_Profiles.begin (); it != g_Profiles.end ();) 369 { 370 if (ts - it->second->GetLastUpdateTime () >= PEER_PROFILE_EXPIRATION_TIMEOUT) 371 it = g_Profiles.erase (it); 372 else 373 it++; 374 } 375 } 376 377 return std::async (std::launch::async, DeleteFilesFromDisk); 378 } 379 380 bool UpdateRouterProfile (const IdentHash& identHash, std::function<void (std::shared_ptr<RouterProfile>)> update) 381 { 382 if (!update) return true; 383 std::shared_ptr<RouterProfile> profile; 384 { 385 std::lock_guard<std::mutex> l(g_ProfilesMutex); 386 auto it = g_Profiles.find (identHash); 387 if (it != g_Profiles.end ()) 388 profile = it->second; 389 } 390 if (profile) 391 { 392 update (profile); 393 return true; 394 } 395 // postpone 396 std::lock_guard<std::mutex> l(g_PostponedUpdatesMutex); 397 g_PostponedUpdates.emplace_back (identHash, update); 398 return false; 399 } 400 401 static void ApplyPostponedUpdates (std::list<std::pair<i2p::data::IdentHash, std::function<void (std::shared_ptr<RouterProfile>)> > >&& updates) 402 { 403 for (const auto& [ident, update] : updates) 404 { 405 auto profile = GetRouterProfile (ident); 406 update (profile); 407 } 408 } 409 410 std::future<void> FlushPostponedRouterProfileUpdates () 411 { 412 if (g_PostponedUpdates.empty ()) return std::future<void>(); 413 414 std::list<std::pair<i2p::data::IdentHash, std::function<void (std::shared_ptr<RouterProfile>)> > > updates; 415 { 416 std::lock_guard<std::mutex> l(g_PostponedUpdatesMutex); 417 g_PostponedUpdates.swap (updates); 418 } 419 return std::async (std::launch::async, ApplyPostponedUpdates, std::move (updates)); 420 } 421 } 422 }