FusionAds-iOS/FusionAds/Classes/fusion/engine/mab/base/MabAuctioneer.swift

545 lines
15 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import Foundation
// AggregatorBid
private struct AggregatorBid {
let config: AggregatorConfig
/**
*
* show revenue * selfRaise bid
* true[floor]revenue
*/
let useSelfRaise: Bool
/**
* bidnil使winnersrevenue, see [MabWinners.maxRevenue]
*
*/
let floor: Double?
/**
*
*/
let reason: RequestReason?
init(config: AggregatorConfig, useSelfRaise: Bool = false, floor: Double? = nil, reason: RequestReason? = nil) {
self.config = config
self.useSelfRaise = useSelfRaise
self.floor = floor
self.reason = reason
}
var raiseRate: Double {
return useSelfRaise ? config.selfRaiseRate ?? config.raiseRate : config.raiseRate
}
}
//
extension AggregatorConfig {
var deferRetryUntilShown: Bool {
return adPlatform == AdPlatform.adMob.name
}
}
/**
* 广
*/
private class MabWinners<T: MabBidder> {
private var winners: [String: T] = [:]
private let onEvict: ((T) -> Void)?
init(onEvict: ((T) -> Void)? = nil) {
self.onEvict = onEvict
}
/**
*
*/
func evictCache() {
let expiredKeys = winners.filter { (_, bidder) in
if bidder.isExpired != false {
onEvict?(bidder)
return true
}
return false
}.map { $0.key }
for key in expiredKeys {
winners.removeValue(forKey: key)
}
}
/**
* 广
*/
func add(bidder: T) {
evictCache()
winners[bidder.aggregatorId] = bidder
}
/**
* 广
*/
func contains(aggregatorId: String) -> Bool {
evictCache()
return winners[aggregatorId] != nil
}
/**
* 广广0
*/
func maxRevenue() -> Double {
evictCache()
if winners.isEmpty {
return 0.0
}
return winners.values.map { $0.revenue }.max() ?? 0.0
}
/**
* 广
*/
func peekMaxRevenue() -> T? {
evictCache()
return winners.values.max(by: {
if($0.revenue == $1.revenue) {
//
return $0.config.priority > $1.config.priority
}
return $0.revenue < $1.revenue
})
}
/**
* 广
*/
func pollMaxRevenue() -> T? {
evictCache()
guard let entry = peekMaxRevenue() else {
return nil
}
return winners.removeValue(forKey: entry.aggregatorId)
}
/**
* 广
*/
func isEmpty() -> Bool {
evictCache()
return winners.isEmpty
}
/**
* 广
*/
func clear() {
winners.removeAll()
}
func buildStateLog() -> String {
var result = "winners["
for winner in winners {
result += "\(winner.key)(\(winner.value.revenue)),"
}
result += "]"
return result
}
}
/**
* loadAdMobloadloadloadload广show
* -> "广showload"
*/
private class RetryGate {
// SwiftAtomicInteger使
private class AtomicInteger {
private var value: Int
init(_ initialValue: Int = 0) {
self.value = initialValue
}
func get() -> Int {
return value
}
func set(_ newValue: Int) {
value = newValue
}
func incrementAndGet() -> Int {
value += 1
return value
}
}
private var version = AtomicInteger() // ad show
private var lastUsed: [String: AtomicInteger] = [:]
func onAdShown() {
_ = version.incrementAndGet()
}
/**
* showretryshow
*/
func canRetry(aggregatorId: String) -> Bool {
return getLastUsed(aggregatorId: aggregatorId).get() < version.get()
}
/**
*
*/
func markRetryUsed(aggregatorId: String) {
getLastUsed(aggregatorId: aggregatorId).set(version.get())
}
private func getLastUsed(aggregatorId: String) -> AtomicInteger {
if let lastUsed = lastUsed[aggregatorId] {
return lastUsed
} else {
let atomicInteger = AtomicInteger(-1)
lastUsed[aggregatorId] = atomicInteger
return atomicInteger
}
}
}
private func selectAdUnitSpec(
aggregatorConfig: AggregatorConfig,
floor: Double,
selfRaise: Bool = false
) -> AdUnitSpec? {
let adUnitIds = aggregatorConfig.adUnitIds
if adUnitIds.isEmpty {
return nil
}
let targetFloor = floor * (selfRaise ? aggregatorConfig.selfRaiseRate ?? aggregatorConfig.raiseRate : aggregatorConfig.raiseRate)
let firstGtIndex = adUnitIds.firstIndex { $0.floor >= targetFloor }
if firstGtIndex == nil {
return adUnitIds.last
}
if firstGtIndex == 0 {
return adUnitIds.first
}
// targetFloor
let index = firstGtIndex!
let nearestIndex = (abs(targetFloor - adUnitIds[index].floor) <= abs(adUnitIds[index - 1].floor - targetFloor)) ? index : index - 1
let selection = adUnitIds[nearestIndex]
return selection
}
enum RequestReason: String {
case INIT = "init"
case FAIL = "fail"
case SHOW = "show"
case EXPIRATION = "exp"
var statsName: String {
return rawValue
}
}
protocol MabBidder {
var id: Int { get }
var config: AggregatorConfig { get }
var isExpired: Bool? { get }
var revenue: Double { get }
func load(reason: RequestReason?) -> Bool
func destroy()
}
extension MabBidder {
var aggregatorId: String {
return config.aggregatorId
}
}
//
class MabBidderUtils {
static func adjustRevenue(ad: FusionAd, floor: Double) -> Double {
switch ad.adPlatform {
case .max:
return ad.revenue
case .ironSource:
return max(ad.revenue, floor)
case .adMob:
return max(ad.revenue, floor)
default:
return max(ad.revenue, floor)
}
}
}
class MabAuctioneer<T: MabBidder> {
enum RequestResult {
case success
case busy
case noAvailableSource
}
private let tag: String
private let faUnitId: String
private let priorityAggregators: [AggregatorConfig]
private var candidatesQueue: [AggregatorBid] = []
private let bidderFactory: (AggregatorConfig, AdUnitSpec) -> T?
private var bidding: T?
private var bidders: [String: T] = [:]
private let winners: MabWinners<T>
private let retryGate = RetryGate()
var hasWinner: Bool {
return !winners.isEmpty()
}
init(
engineId: Int,
faUnitId: String,
aggregatorConfigs: [AggregatorConfig],
bidderFactory: @escaping (AggregatorConfig, AdUnitSpec) -> T?
) {
self.tag = "MabAuctioneer@\(engineId)"
self.faUnitId = faUnitId
self.priorityAggregators = aggregatorConfigs.sorted(by: { $0.priority < $1.priority })
self.bidderFactory = bidderFactory
self.winners = MabWinners<T>()
}
private func logI(_ message: String) {
Logger.i(tag: tag, message: message)
}
private func logD(_ message: String) {
Logger.d(tag: tag, message: message)
}
private func logW(_ message: String) {
Logger.w(tag: tag, message: message)
}
private func logE(_ message: String) {
Logger.e(tag: tag, message: message)
}
private func logState() {
logI("mab state: \(buildStateLog())")
}
private func obtainBidder(
config: AggregatorConfig,
adUnitSpec: AdUnitSpec
) -> T? {
if let bidder = bidders[adUnitSpec.adUnitId] {
return bidder
} else {
if let newBidder = bidderFactory(config, adUnitSpec) {
bidders[adUnitSpec.adUnitId] = newBidder
return newBidder
}
return nil
}
}
func requestNext() -> RequestResult {
logI("requestNext")
let result = internalRequestNext()
logI("requestNext result \(result)")
logState()
return result
}
private func internalRequestNext() -> RequestResult {
if bidding != nil {
return .busy
}
while !candidatesQueue.isEmpty {
guard let bid = candidatesQueue.first else {
break
}
candidatesQueue.removeFirst()
if winners.contains(aggregatorId: bid.config.aggregatorId) {
// bid
continue
}
guard let adUnitSpec = selectAdUnitSpec(
aggregatorConfig: bid.config,
floor: bid.floor ?? winners.maxRevenue(),
selfRaise: bid.useSelfRaise
) else {
continue
}
logD("selected \(adUnitSpec.adUnitId) with \(adUnitSpec.floorEcpm)")
if let bidder = obtainBidder(config: bid.config, adUnitSpec: adUnitSpec) {
if bidder.load(reason: bid.reason) {
bidding = bidder
return .success
}
}
}
return .noAvailableSource
}
func refillCandidatesQueue(reason: RequestReason? = nil) {
logI("refilling candidates")
logState()
for aggregator in priorityAggregators {
fillCandidate(AggregatorBid(config: aggregator, useSelfRaise: false, reason: reason))
}
logI("candidates refilled")
logState()
}
private func fillCandidate(_ bid: AggregatorBid) {
let aggregator = bid.config
//
if winners.contains(aggregatorId: aggregator.aggregatorId) {
logD("refilling - \(aggregator.aggregatorId) filled, skip")
return
}
//
if let currentBidding = bidding, currentBidding.aggregatorId == aggregator.aggregatorId {
logW("refilling - \(aggregator.aggregatorId) is loading, skip")
return
}
// show
if aggregator.deferRetryUntilShown {
if !retryGate.canRetry(aggregatorId: aggregator.aggregatorId) {
logD("refilling - \(aggregator.aggregatorId) deferred until next shown")
return
}
retryGate.markRetryUsed(aggregatorId: aggregator.aggregatorId)
}
logD("refilling - \(aggregator.aggregatorId) added to queue")
candidatesQueue.append(bid)
}
private func onCacheEvict(_ bidder: T) {
// candidatesQueue.append(AggregatorBid(config: bidder.config, reason: .EXPIRATION))
}
func reset() {
logI("reset")
for bidder in bidders.values {
bidder.destroy()
}
bidding?.destroy()
bidding = nil
winners.clear()
bidders.removeAll()
candidatesQueue.removeAll()
refillCandidatesQueue()
}
func onShown(winner: T) {
logI("winner shown, \(winner)")
logState()
retryGate.onAdShown()
// hidden
candidatesQueue.removeAll()
//
if winner.config.selfRaiseRate != nil {
// show
candidatesQueue.append(
AggregatorBid(
config: winner.config,
useSelfRaise: true,
floor: winner.revenue,
reason: .SHOW
)
)
}
}
func onLoaded(bidderId: Int) {
logI("onLoaded \(bidderId)")
if let bidding = bidding, bidding.id == bidderId {
winners.add(bidder: bidding)
self.bidding = nil
} else {
logW("onLoaded bidderId mismatch \(String(describing: bidding?.id)) != \(bidderId)")
}
logState()
}
func onLoadFailed(bidderId: Int) {
logI("onLoadFailed \(bidderId)")
if let bidder = bidding, bidder.id == bidderId {
bidding = nil
} else {
logW("onLoadFailed bidderId mismatch \(String(describing: bidding?.id)) != \(bidderId)")
}
logState()
}
func pollMaxRevenue() -> T? {
return winners.pollMaxRevenue()
}
func peekMaxRevenue() -> T? {
return winners.peekMaxRevenue()
}
func evictCache() {
winners.evictCache()
}
private func buildStateLog() -> String {
var result = "faUnitId(\(faUnitId)), "
result += winners.buildStateLog()
result += ", "
result += "bidding["
if let bidding = bidding {
result += "\(bidding)"
} else {
result += "null"
}
result += "], "
result += "candidates["
for bid in candidatesQueue {
result += "\(bid.config.aggregatorId)("
result += "\(bid.raiseRate)"
if bid.useSelfRaise {
result += " self-raise"
}
result += "), "
}
result += "]"
return result
}
}